Skip to content

Conversation

@Shironex
Copy link
Collaborator

@Shironex Shironex commented Jan 15, 2026

Summary

  • Comprehensive migration from manual data fetching to TanStack React Query v5
  • Adds caching, deduplication, automatic refetching, and optimistic updates
  • Reduces code complexity by ~1,300 lines (2,530 deletions vs 1,194 additions)

Changes Included (11 commits)

Foundation

  • Install @tanstack/react-query and devtools
  • Add QueryClient with default configurations
  • Create query-keys factory for cache key management
  • Wrap app with QueryClientProvider

Query Hooks

  • Features, worktrees, GitHub (issues, PRs, validations, comments)
  • Usage queries with polling, running agents
  • Settings (global, project, credentials)
  • Models, sessions, ideation, CLI status
  • Cursor permissions, workspace directories
  • Pipeline config, spec files

Mutation Hooks

  • Feature CRUD with optimistic updates
  • Auto-mode (start, stop, approve plan)
  • Worktree operations (create, delete, checkout)
  • Settings updates, GitHub PR creation
  • Cursor permissions, spec mutations

Infrastructure

  • WebSocket event to React Query cache bridge
  • Shared skeleton component (replaces 4 duplicates)

Component Migrations

  • Board view, worktree panel
  • GitHub issues/PRs views
  • Settings view tabs
  • Usage popovers, running agents
  • Session manager, spec view, ideation

Test plan

  • Verify board view loads features correctly
  • Test feature CRUD operations
  • Confirm WebSocket events trigger cache invalidation
  • Test usage popovers with polling
  • Verify settings changes persist
  • Test GitHub views load data
  • Check React Query DevTools for proper caching

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added loading skeleton component for improved placeholder visuals.
  • Refactor

    • Modernized data fetching architecture with improved caching, polling, and state synchronization across the application.
    • Enhanced error handling and user feedback with consistent toast notifications.
    • Streamlined loading indicators and refresh mechanisms for better UX consistency.

✏️ Tip: You can customize this high-level summary in your review settings.

Shironex and others added 11 commits January 15, 2026 16:20
- Install @tanstack/react-query and @tanstack/react-query-devtools
- Add QueryClient with default stale times and retry config
- Create query-keys.ts factory for consistent cache key management
- Wrap app root with QueryClientProvider and DevTools

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add useFeatures, useFeature, useAgentOutput for feature data
- Add useGitHubIssues, useGitHubPRs, useGitHubValidations, useGitHubIssueComments
- Add useClaudeUsage, useCodexUsage with polling intervals
- Add useRunningAgents, useRunningAgentsCount
- Add useWorktrees, useWorktreeInfo, useWorktreeStatus, useWorktreeDiffs
- Add useGlobalSettings, useProjectSettings, useCredentials
- Add useAvailableModels, useCodexModels, useOpencodeModels
- Add useSessions, useSessionHistory, useSessionQueue
- Add useIdeationPrompts, useIdeas
- Add CLI status queries (claude, cursor, codex, opencode, github)
- Add useCursorPermissionsQuery, useWorkspaceDirectories
- Add usePipelineConfig, useSpecFile, useSpecRegenerationStatus

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add feature mutations (create, update, delete with optimistic updates)
- Add auto-mode mutations (start, stop, approve plan)
- Add worktree mutations (create, delete, checkout, switch branch)
- Add settings mutations (update global/project, validate API keys)
- Add GitHub mutations (create PR, validate PR)
- Add cursor permissions mutations (apply profile, copy config)
- Add spec mutations (generate, update, save)
- Add pipeline mutations (toggle, update config)
- Add session mutations with cache invalidation

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add useAutoModeQueryInvalidation for feature/agent events
- Add useSpecRegenerationQueryInvalidation for spec updates
- Add useGitHubValidationQueryInvalidation for PR validation events
- Bridge WebSocket events to cache invalidation for real-time updates

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Add reusable SkeletonPulse component to replace 4 duplicate definitions
- Update CLI status components to use shared skeleton
- Simplify CLI status components by using React Query hooks

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Replace manual fetching in use-board-features with useFeatures query
- Migrate use-board-actions to use mutation hooks
- Update kanban-card and agent-info-panel to use query hooks
- Migrate agent-output-modal to useAgentOutput query
- Migrate create-pr-dialog to useCreatePR mutation
- Remove manual loading/error state management
- Add proper cache invalidation on mutations

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Migrate use-worktrees to useWorktrees query hook
- Migrate use-branches to useWorktreeBranches query hook
- Migrate use-available-editors to useAvailableEditors query hook
- Migrate use-worktree-actions to use mutation hooks
- Update worktree-panel component to use query data
- Remove manual state management for loading/errors

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Migrate use-github-issues to useGitHubIssues query
- Migrate use-issue-comments to useGitHubIssueComments infinite query
- Migrate use-issue-validation to useGitHubValidations with mutations
- Migrate github-prs-view to useGitHubPRs query
- Support pagination for comments with useInfiniteQuery
- Remove manual loading state management

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Migrate use-cursor-permissions to query and mutation hooks
- Migrate use-cursor-status to React Query
- Migrate use-skills-settings to useUpdateGlobalSettings mutation
- Migrate use-subagents-settings to mutation hooks
- Migrate use-subagents to useDiscoveredAgents query
- Migrate opencode-settings-tab to React Query hooks
- Migrate worktrees-section to query hooks
- Migrate codex/claude usage sections to query hooks
- Remove manual useState for loading/error states

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Migrate claude-usage-popover to useClaudeUsage query with polling
- Migrate codex-usage-popover to useCodexUsage query with polling
- Migrate usage-popover to React Query hooks
- Migrate running-agents-view to useRunningAgents query
- Replace manual polling intervals with refetchInterval
- Remove manual loading/error state management

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Migrate workspace-picker-modal to useWorkspaceDirectories query
- Migrate session-manager to useSessions query
- Migrate git-diff-panel to useGitDiffs query
- Migrate prompt-list to useIdeationPrompts query
- Migrate spec-view hooks to useSpecFile query and spec mutations
- Migrate use-board-background-settings to useProjectSettings query
- Migrate use-guided-prompts to useIdeationPrompts query
- Migrate use-project-settings-loader to React Query
- Complete React Query migration across all components

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

This PR introduces React Query as a centralized data-fetching and state-management solution across the UI. It replaces manual API calls, local state management, and direct Electron API usage with declarative query and mutation hooks, adds comprehensive cache management with configurable stale times, and wires query invalidation for cache coherence. Over 80 files are refactored to consume these hooks.

Changes

Cohort / File(s) Summary
React Query Infrastructure
package.json, apps/ui/src/lib/query-client.ts, apps/ui/src/lib/query-keys.ts, apps/ui/src/routes/__root.tsx
Added React Query and DevTools dependencies; created centralized query client with STALE_TIMES and GC_TIMES constants; implemented global error handling, retry logic, and auth-aware strategies; generated comprehensive query key factory for all domains (features, worktrees, GitHub, settings, etc.); wrapped root layout with QueryClientProvider and DevTools.
Query Hooks — Core Data Fetching
apps/ui/src/hooks/queries/index.ts, apps/ui/src/hooks/queries/use-features.ts, apps/ui/src/hooks/queries/use-worktrees.ts, apps/ui/src/hooks/queries/use-github.ts, apps/ui/src/hooks/queries/use-usage.ts, apps/ui/src/hooks/queries/use-settings.ts, apps/ui/src/hooks/queries/use-models.ts, apps/ui/src/hooks/queries/use-cli-status.ts, apps/ui/src/hooks/queries/use-running-agents.ts, apps/ui/src/hooks/queries/use-sessions.ts, apps/ui/src/hooks/queries/use-git.ts, apps/ui/src/hooks/queries/use-pipeline.ts, apps/ui/src/hooks/queries/use-spec.ts, apps/ui/src/hooks/queries/use-cursor-permissions.ts, apps/ui/src/hooks/queries/use-workspace.ts, apps/ui/src/hooks/queries/use-ideation.ts
Created 15 new query hook modules providing type-safe data fetching for features, worktrees, GitHub issues/PRs/validations, usage statistics, settings, models, CLI tool statuses, running agents, sessions, diffs, pipeline config, spec files, cursor permissions, workspace directories, and ideation prompts. Each hook validates inputs, handles errors, and applies appropriate stale times and enabled guards.
Mutation Hooks — Server Operations
apps/ui/src/hooks/mutations/index.ts, apps/ui/src/hooks/mutations/use-feature-mutations.ts, apps/ui/src/hooks/mutations/use-auto-mode-mutations.ts, apps/ui/src/hooks/mutations/use-worktree-mutations.ts, apps/ui/src/hooks/mutations/use-github-mutations.ts, apps/ui/src/hooks/mutations/use-spec-mutations.ts, apps/ui/src/hooks/mutations/use-settings-mutations.ts, apps/ui/src/hooks/mutations/use-ideation-mutations.ts, apps/ui/src/hooks/mutations/use-cursor-permissions-mutations.ts
Created 8 new mutation hook modules encapsulating all write operations (create/update/delete features, worktrees, specs; start/stop/verify auto mode; validate GitHub issues; apply cursor profiles). Each mutation includes success/error handlers, cache invalidation, and user feedback via toasts. Barrel export provides single import point.
Usage & Settings Components
apps/ui/src/components/usage-popover.tsx, apps/ui/src/components/claude-usage-popover.tsx, apps/ui/src/components/codex-usage-popover.tsx, apps/ui/src/components/views/settings-view/api-keys/claude-usage-section.tsx, apps/ui/src/components/views/settings-view/codex/codex-usage-section.tsx
Replaced local state and manual polling with React Query hooks (useClaudeUsage, useCodexUsage). Removed auto-refresh intervals and direct Electron API calls. Simplified error handling with normalized error messages. Updated UI to use isFetching for spinner states and refetch() for manual refresh. Introduced dataUpdatedAt for staleness checks.
Worktree & Board View Components
apps/ui/src/components/views/board-view.tsx, apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx, apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts, apps/ui/src/components/views/board-view/worktree-panel/hooks/use-branches.ts, apps/ui/src/components/views/board-view/worktree-panel/hooks/use-available-editors.ts, apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts
Migrated worktree/branch data fetching to React Query; removed manual load effects and local state; introduced useWorktreesQuery and useWorktreeBranches; refactored useWorktreeActions from accepting fetch callbacks to using mutations directly (useSwitchBranch, usePullWorktree, usePushWorktree, useOpenInEditor); added current-worktree validation with refs to prevent race conditions.
Session & Running Agents
apps/ui/src/components/session-manager.tsx, apps/ui/src/components/views/running-agents-view.tsx
Replaced local session state with useSessions() query; removed manual loadSessions; introduced query invalidation (invalidateSessions) after mutations. Replaced running agents polling with useRunningAgents() query; added useStopFeature mutation for agent control. Updated UI to derive counts and content from query data.
Board View Dialogs & Sub-components
apps/ui/src/components/views/board-view/dialogs/workspace-picker-modal.tsx, apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx, apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx, apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx, apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx
Introduced useWorkspaceDirectories query for modal; added path field to WorkspaceDirectory. Replaced agent output loading with useAgentOutput query; added resolvedProjectPath fallback logic. Replaced branch fetching with useWorktreeBranches mutation. Added real-time agent data fetching (useFeature, useAgentOutput) to AgentInfoPanel; introduced projectPath prop; added polling/fetch controls.
Board Actions & Features
apps/ui/src/components/views/board-view/hooks/use-board-actions.ts, apps/ui/src/components/views/board-view/hooks/use-board-features.ts
Replaced direct API calls in handleVerifyFeature/handleResumeFeature with useVerifyFeature/useResumeFeature mutations. Replaced manual feature loading with useFeatures query; removed direct useAppStore state mutations; introduced invalidateSessions for cache refresh. Simplified category persistence; removed spec-regeneration subscriptions; added auto-mode event listener with ding sound and error handling.
Git & Panel Components
apps/ui/src/components/ui/git-diff-panel.tsx
Replaced Electron API loading with two conditional data hooks: useWorktreeDiffs (when useWorktrees=true) and useGitDiffs (otherwise). Removed manual load effect; delegated refetch to appropriate hook; simplified error/loading state derivation.
Skeleton Component
apps/ui/src/components/ui/skeleton.tsx
Added new SkeletonPulse component with pulsing animation. Exported from central location for reuse across CLI status and other skeleton-based UIs.
CLI Status Components
apps/ui/src/components/views/settings-view/cli-status/claude-cli-status.tsx, apps/ui/src/components/views/settings-view/cli-status/codex-cli-status.tsx, apps/ui/src/components/views/settings-view/cli-status/cursor-cli-status.tsx, apps/ui/src/components/views/settings-view/cli-status/opencode-cli-status.tsx
Removed locally defined SkeletonPulse; imported from centralized @/components/ui/skeleton module.
Settings Hooks & Cursor
apps/ui/src/components/views/settings-view/hooks/use-cursor-permissions.ts, apps/ui/src/components/views/settings-view/hooks/use-cursor-status.ts
Replaced local state and HTTP calls with React Query hooks (useCursorPermissionsQuery, useCursorCliStatus); introduced mutations for applyProfile and copyConfig; added status transformation via useMemo; synced to global setup store; exported PermissionsData as type alias.
Claude Settings
apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-skills-settings.ts, apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-subagents-settings.ts, apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-subagents.ts
Replaced direct Electron API calls with useUpdateGlobalSettings mutation; converted updateEnabled/updateSources to useCallback wrappers; replaced local loading state with mutation.isPending. Added query hook for discovering filesystem agents (useDiscoveredAgents); exported FilesystemSource and SubagentWithScope types.
OpenCode Settings
apps/ui/src/components/views/settings-view/providers/opencode-settings-tab.tsx
Replaced manual CLI status and model loading with useOpencodeCliStatus, useOpencodeProviders, useOpencodeModels queries. Consolidated handleRefreshOpencodeCli to use queryClient invalidation. Removed legacy state variables (isCheckingOpencodeCli, cachedOpencodeProviders). Updated skeleton rendering to be data-driven.
Worktrees Section
apps/ui/src/components/views/settings-view/worktrees/worktrees-section.tsx
Replaced direct API calls for init script with useWorktreeInitScript, useSetInitScript, useDeleteInitScript hooks. Added local state for scriptContent/originalContent; derived saving/deleting states from mutations. Removed explicit loading state management.
GitHub Issues & PRs
apps/ui/src/components/views/github-issues-view/hooks/use-github-issues.ts, apps/ui/src/components/views/github-issues-view/hooks/use-issue-comments.ts, apps/ui/src/components/views/github-issues-view/hooks/use-issue-validation.ts, apps/ui/src/components/views/github-prs-view.tsx
Replaced manual issue/PR fetching with useGitHubIssues, useGitHubPRs queries. Replaced infinite pagination with useInfiniteQuery for comments. Added validateIssueMutation and markViewedMutation hooks; updated validation workflow to use mutations. Refactored PR view to use dedicated useGitHubPRs hook; removed logger usage.
Ideation
apps/ui/src/components/views/ideation-view/components/prompt-list.tsx
Replaced Electron API calls for ideation with useGenerateIdeationSuggestions mutation. Added generateMutation.isPending gating for prompts. Replaced try/catch with onSuccess/onError callbacks. Updated job status, toasts, and navigation on completion.
Spec View
apps/ui/src/components/views/spec-view/hooks/use-spec-generation.ts, apps/ui/src/components/views/spec-view/hooks/use-spec-loading.ts, apps/ui/src/components/views/spec-view/hooks/use-spec-save.ts
Introduced useCreateSpec, useRegenerateSpec, useGenerateFeatures, useSaveSpec mutations for generation and saving; removed try/catch around API calls. Replaced manual spec loading with useSpecFile and useSpecRegenerationStatus queries. Added loadSpec function that invalidates and conditionally refetches. Replaced writeFile calls with mutation-based saving.
Utility Hooks
apps/ui/src/hooks/use-board-background-settings.ts, apps/ui/src/hooks/use-guided-prompts.ts, apps/ui/src/hooks/use-project-settings-loader.ts, apps/ui/src/hooks/use-query-invalidation.ts
Refactored board-background persistence to use useUpdateProjectSettings mutation. Replaced manual prompt fetching with useIdeationPrompts query. Replaced HTTP-based project settings loading with useProjectSettings query; added settingsMap for applying UI preferences. Added comprehensive useQueryInvalidation hook with event listeners for auto-mode, spec regeneration, GitHub validation, and session events to trigger cache invalidation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Enhancement, Do Not Merge

Poem

🐰 wiggles nose with delight
The queries dance in harmony true,
From local chaos to patterns anew,
Cache whispers sweetly, fresh data takes flight,
Invalidations bloom in the React Query night,
No more fetch fumbles—just clarity and might! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(ui): migrate to React Query for data fetching' accurately and concisely describes the main change: a comprehensive migration of the UI layer from manual data fetching to React Query v5.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @Shironex, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant refactor of the UI's data fetching mechanism, replacing manual implementations with TanStack React Query. This change aims to improve the application's responsiveness, reduce code duplication, and provide a more maintainable architecture for data management. The PR also includes several infrastructure improvements and component migrations to fully leverage React Query's capabilities.

Highlights

  • Migration to React Query: This PR migrates the UI from manual data fetching to TanStack React Query v5, simplifying data management and improving performance.
  • Code Reduction: The migration reduces code complexity by approximately 1,300 lines, primarily through the removal of manual data fetching logic.
  • New Features: Adds caching, deduplication, automatic refetching, and optimistic updates to improve the user experience.
  • Infrastructure Improvements: Includes a WebSocket event to React Query cache bridge and a shared skeleton component, enhancing data synchronization and UI consistency.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request significantly refactors the UI's data fetching and state management by migrating numerous components from manual useState/useEffect patterns and Zustand store interactions to TanStack React Query. Key changes include upgrading React Query dependencies, introducing new query and mutation hooks (useClaudeUsage, useCodexUsage, useWorkspaceDirectories, useSessions, useWorktreeDiffs, useGitDiffs, usePipelineConfig, useFeature, useAgentOutput, useWorktreeBranches, useVerifyFeature, useResumeFeature, useFeatures, useAvailableEditors, useWorktreeInitScript, useGithubIssues, useGitHubIssueComments, useValidateIssue, useMarkValidationViewed, useGitHubPRs, useGenerateIdeationSuggestions, useRunningAgents, useStopFeature, useSetInitScript, useDeleteInitScript, useCreateSpec, useRegenerateSpec, useGenerateFeatures, useSaveSpec, useCursorPermissionsQuery, useApplyCursorProfile, useCopyCursorConfig, useCursorCliStatus, useUpdateGlobalSettings, useUpdateProjectSettings, useSubagentsSettings, useSkillsSettings, useDiscoveredAgents), and centralizing query invalidation logic based on WebSocket events. Many components now derive loading, error, and data states directly from React Query hooks, simplifying their internal logic. A new SkeletonPulse component was added for loading indicators. Review comments highlight the need to map generic React Query errors back to more specific, user-friendly messages in some usage popovers, to fully migrate components to React Query as the single source of truth for data (rather than dual-updating Zustand), and to consider adding generic toast notifications for query errors in the global error handler.

@Shironex
Copy link
Collaborator Author

@coderabbitai review this

@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

@Shironex: I'll review the changes in this PR. This looks like a comprehensive migration to React Query - I'll examine the implementation of query/mutation hooks, the WebSocket integration, component migrations, and verify best practices for React Query v5 are being followed.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx (1)

85-250: Missing onClose in useEffect dependency array.

The onClose callback is invoked at line 234 but is not included in the dependency array at line 250. This can cause a stale closure bug where an outdated onClose reference is called if the parent re-renders with a new callback.

Proposed fix
     return () => {
       unsubscribe();
     };
-  }, [open, featureId]);
+  }, [open, featureId, onClose]);
apps/ui/src/components/views/github-issues-view/hooks/use-issue-validation.ts (1)

264-287: Call resolveModelString() before passing model to mutation.

Per coding guidelines for TypeScript files, model aliases (haiku, sonnet, opus) must be resolved to full model names using resolveModelString() from @automaker/model-resolver. Currently, modelToUse is passed directly to the mutation without resolution. Apply the same pattern used in model-config.ts:

const modelToUse = resolveModelString(normalizedEntry.model);

Import: import { resolveModelString } from '@automaker/model-resolver';

apps/ui/src/components/views/github-prs-view.tsx (1)

380-391: group-hover won't work — parent lacks group class.

The button uses group-hover:opacity-100 but the parent <div> in PRRow (line 315) doesn't have the group class. The button will remain invisible on hover.

Suggested fix

Add group class to the parent div:

   <div
     className={cn(
-      'flex items-start gap-3 p-3 cursor-pointer hover:bg-accent/50 transition-colors',
+      'group flex items-start gap-3 p-3 cursor-pointer hover:bg-accent/50 transition-colors',
       isSelected && 'bg-accent'
     )}
     onClick={onClick}
   >
🤖 Fix all issues with AI agents
In `@apps/ui/src/components/session-manager.tsx`:
- Around line 155-160: The useEffect currently lists a boolean expression
([sessions.length > 0]) which is unstable and causes ESLint/misrun issues;
update it to use a stable dependency list and a ref to run only once on initial
load: add a useRef flag (e.g., firstLoadRef) and inside the useEffect that
depends on sessions (or sessions.length) check if firstLoadRef.current is false
and sessions.length > 0 then call checkRunningSessions(sessions) and set
firstLoadRef.current = true; ensure checkRunningSessions and sessions are
included in the dependency array (or memoize checkRunningSessions) so ESLint
warnings are resolved.

In `@apps/ui/src/components/usage-popover.tsx`:
- Around line 137-139: The wrapper functions fetchClaudeUsage and
fetchCodexUsage are defined with no parameters but are invoked with a boolean
(false) elsewhere; update either the call sites to remove the unused argument or
change the wrappers to accept a parameter and forward it to
refetchClaude/refetchCodex (e.g., add a param like force?: boolean and call
refetchClaude(force) / refetchCodex(force)) so the signatures match the usages;
adjust all occurrences (including the calls around lines ~302-311, 307, 414) for
consistency.

In
`@apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx`:
- Line 100: Update the import paths in kanban-card.tsx and agent-info-panel.tsx
to follow the `@automaker/`* package pattern: replace imports from '@/lib/utils'
with '@automaker/utils', replace '@/lib/agent-context-parser' with
'@automaker/agent-context-parser', and replace '@/store/app-store' with the
appropriate `@automaker` package (e.g., '@automaker/store' or
'@automaker/app-store' depending on the published package name used elsewhere in
the repo); adjust the import lines in those two files to use these `@automaker/`*
module specifiers, ensure any exported symbols (e.g., useWorktrees,
currentProject, agent-context parsing helpers) are correctly referenced after
the change, and run type-check/build to confirm the new module paths resolve.

In
`@apps/ui/src/components/views/board-view/worktree-panel/hooks/use-branches.ts`:
- Around line 16-28: The gitRepoStatus defaults currently set isGitRepo: true
and hasCommits: true cause the UI to assume repo capability before
useWorktreeBranches completes; change the construction of gitRepoStatus (where
branchData is read) to default both fields to false (e.g., use
branchData?.isGitRepo ?? false and branchData?.hasCommits ?? false) so the UI
reflects an unknown/unavailable state while isLoadingBranches is true or the
query fails; ensure worktree-actions-dropdown behavior relies on these
conservative values rather than presuming true.

In `@apps/ui/src/components/views/spec-view/hooks/use-spec-loading.ts`:
- Around line 27-47: loadSpec currently calls queryClient.invalidateQueries then
immediately uses queryClient.getQueryData to check status, which can return
stale data; replace that pattern by awaiting the latest status via
queryClient.fetchQuery (or awaiting a refetch) for the queryKey from
queryKeys.specRegeneration.status(currentProject.path) to obtain up-to-date
statusData before checking isRunning, then proceed to invalidate/refetch
queryKeys.spec.file; keep the currentProject?.path guard and existing queryKey
symbols (loadSpec, queryClient.invalidateQueries,
queryKeys.specRegeneration.status, queryKeys.spec.file,
queryClient.getQueryData) but remove the stale getQueryData use in favor of
fetchQuery/awaited refetch.

In `@apps/ui/src/components/views/spec-view/hooks/use-spec-save.ts`:
- Around line 9-10: The mutation is being instantiated with an empty-string
fallback which can produce an invalid path; change the call site so useSaveSpec
receives a valid path or is not created when currentProject is null: either pass
currentProject?.path (allowing undefined) instead of '' or wrap the useSaveSpec
call behind a conditional so saveMutation is only created when currentProject
exists, and/or update useSaveSpec to accept an optional projectPath and guard
inside its saveSpec handler to prevent writes when projectPath is missing;
reference useSaveSpec and saveMutation and ensure any UI checks (e.g., the
existing Line 13 guard) align with this conditional instantiation.

In `@apps/ui/src/hooks/mutations/use-auto-mode-mutations.ts`:
- Around line 103-125: useStopFeature only invalidates runningAgents but not the
features cache, which can leave stale feature status in the UI; update
useStopFeature to also invalidate the features cache after success by calling
queryClient.invalidateQueries for the features key (e.g.,
queryClient.invalidateQueries({ queryKey: queryKeys.features.all() })); if
features are namespaced by projectPath, either add projectPath to the
hook/mutation input and invalidate the specific project features key, or use the
broader queryKey invalidation pattern to clear all features if projectPath isn't
available.

In `@apps/ui/src/hooks/mutations/use-github-mutations.ts`:
- Around line 40-44: The example and mutation code pass a model alias string
directly to the API; call resolveModelString from `@automaker/model-resolver` to
convert the alias to a canonical model identifier before sending the payload.
Import resolveModelString into use-github-mutations.ts and use it to transform
the model field (where model: 'sonnet' or the variable holding model is
assembled) inside the mutation handler or payload builder (e.g., in the function
that constructs the request body in useGithubMutations) so the resolved model
string is sent to the API instead of the alias.

In `@apps/ui/src/hooks/mutations/use-spec-mutations.ts`:
- Around line 155-179: The mutation constructs a file path with projectPath and
will write to "/.automaker/..." if projectPath is empty; update useSaveSpec so
mutationFn validates projectPath before calling api.writeFile (e.g., at the top
of mutationFn check if (!projectPath || projectPath.trim() === '') throw new
Error('Invalid projectPath')); then build the path and call api.writeFile, so
you never attempt to write to the root when projectPath is empty and the onError
handler will surface the error; reference useSaveSpec, its mutationFn,
projectPath and api.writeFile for where to add the guard.

In `@apps/ui/src/hooks/queries/use-settings.ts`:
- Around line 105-122: The query key for useDiscoveredAgents omits the sources
parameter so changes to sources won't invalidate cache; update the queryKey to
include sources (e.g., pass sources or a stable representation like sources ??
[] as an extra segment) when calling queryKeys.settings.agents inside
useDiscoveredAgents, or modify queryKeys.settings.agents to accept a sources
argument and use that in the returned key; keep the rest of the hook
(getElectronAPI(), api.settings.discoverAgents, enabled, staleTime) unchanged.

In `@apps/ui/src/hooks/queries/use-worktrees.ts`:
- Around line 163-206: The query key for useWorktreeBranches is missing the
includeRemote flag so cached results can be wrong; update the query key factory
used by queryKeys.worktrees.branches to accept includeRemote (e.g.,
branches(worktreePath, includeRemote)) and change the call in
useWorktreeBranches to pass includeRemote into queryKey, ensuring the key
uniquely reflects both worktreePath and includeRemote; keep the rest of the
query logic and enabled/staleTime behavior unchanged.

In `@apps/ui/src/hooks/use-query-invalidation.ts`:
- Around line 139-164: In useGitHubValidationQueryInvalidation, the optional
chaining on api.github?.onValidationEvent can leave unsubscribe undefined and
cause the returned cleanup to throw; fix by explicitly checking for api.github
before subscribing (e.g., if (!api.github) return undefined or set unsubscribe
to a noop function) so that unsubscribe is always a callable cleanup; update the
code around getElectronAPI()/api.github and the unsubscribe return so the
cleanup always returns a function (or undefined safely) to avoid calling
undefined.
♻️ Duplicate comments (3)
apps/ui/src/lib/query-client.ts (1)

57-70: Query errors are silently logged without user feedback.

Unlike handleMutationError, this handler only logs errors and doesn't show a toast for general query failures. While background refetch failures may not need toasts, initial query failures could leave users unaware of issues if individual queries don't implement their own onError.

Consider whether this is intentional UX behavior or if a generic toast should be added for consistency with mutation error handling.

apps/ui/src/components/views/board-view.tsx (1)

1571-1577: Dual source of truth remains a concern.

The cache invalidation via React Query followed by Zustand update maintains two sources of truth for pipeline configuration. While the comment acknowledges this is for "backward compatibility," this creates potential for state drift between React Query cache and Zustand store.

Consider tracking a follow-up task to fully migrate consumers to React Query, eliminating the need for the Zustand update. Until then, ensure the invalidation completes before the Zustand update to maintain consistency.

Potential improvement for consistency
         // Invalidate React Query cache to refetch updated config
-        queryClient.invalidateQueries({
+        await queryClient.invalidateQueries({
           queryKey: queryKeys.pipeline.config(currentProject.path),
         });
         // Also update Zustand for backward compatibility
         setPipelineConfig(currentProject.path, config);

Using await ensures the cache is invalidated before updating Zustand, though the real fix is to eliminate the dual state.

apps/ui/src/components/claude-usage-popover.tsx (1)

185-191: Generic error message loses actionable context.

The previous implementation likely had specific error handling (e.g., TRUST_PROMPT) that provided tailored user guidance. The current generic message suggests claude login for all errors, which may not be appropriate for network failures or other non-auth issues.

Consider mapping error types to specific user-friendly messages, similar to how other usage popovers in the codebase handle this.

🧹 Nitpick comments (46)
apps/ui/src/hooks/use-project-settings-loader.ts (2)

29-32: Consider resetting the ref when settings change to avoid stale state.

The appliedProjectRef prevents re-application when the same project stays selected. However, if settings are updated elsewhere (e.g., another client or settings panel) and React Query refetches, the new values won't be applied because the ref still matches the current project path.

If re-applying updated settings is desired, consider keying the ref on a combination of project path and a settings version/timestamp, or removing the guard and relying on setter idempotency.


55-72: Type safety could be improved in the settingsMap iteration.

The cast on line 70 loses type information. While this works at runtime if the API types align, TypeScript won't catch mismatches between setting keys and their expected value types (e.g., cardOpacity expects a number, columnBorderEnabled expects a boolean).

If you prefer the iteration pattern, consider a type-safe approach:

♻️ Suggested improvement
-    // Settings map for cleaner iteration
-    const settingsMap = {
-      cardOpacity: setCardOpacity,
-      columnOpacity: setColumnOpacity,
-      columnBorderEnabled: setColumnBorderEnabled,
-      cardGlassmorphism: setCardGlassmorphism,
-      cardBorderEnabled: setCardBorderEnabled,
-      cardBorderOpacity: setCardBorderOpacity,
-      hideScrollbar: setHideScrollbar,
-    } as const;
-
-    // Apply all settings that are defined
-    for (const [key, setter] of Object.entries(settingsMap)) {
-      const value = bg?.[key as keyof typeof bg];
-      if (value !== undefined) {
-        (setter as (path: string, val: typeof value) => void)(projectPath, value);
-      }
-    }
+    // Apply all board background settings that are defined
+    if (bg?.cardOpacity !== undefined) setCardOpacity(projectPath, bg.cardOpacity);
+    if (bg?.columnOpacity !== undefined) setColumnOpacity(projectPath, bg.columnOpacity);
+    if (bg?.columnBorderEnabled !== undefined) setColumnBorderEnabled(projectPath, bg.columnBorderEnabled);
+    if (bg?.cardGlassmorphism !== undefined) setCardGlassmorphism(projectPath, bg.cardGlassmorphism);
+    if (bg?.cardBorderEnabled !== undefined) setCardBorderEnabled(projectPath, bg.cardBorderEnabled);
+    if (bg?.cardBorderOpacity !== undefined) setCardBorderOpacity(projectPath, bg.cardBorderOpacity);
+    if (bg?.hideScrollbar !== undefined) setHideScrollbar(projectPath, bg.hideScrollbar);

The explicit approach trades brevity for full type safety—TypeScript will catch any type mismatches at compile time.

apps/ui/src/hooks/use-board-background-settings.ts (2)

16-24: await on mutate() has no effect; use mutateAsync() if awaiting is intended.

persistSettings calls updateProjectSettings.mutate(), which is fire-and-forget and returns void. The await in callers like setBoardBackground (line 63) does nothing. This pattern repeats across all wrapper functions.

If you need to wait for the mutation to complete (e.g., for error handling or sequencing), use mutateAsync() instead. Otherwise, remove the async/await keywords to avoid confusion.

Option 1: Use mutateAsync for true async behavior
  const persistSettings = useCallback(
    (projectPath: string, settingsToUpdate: Record<string, unknown>) => {
-     updateProjectSettings.mutate({
+     return updateProjectSettings.mutateAsync({
        projectPath,
        settings: { boardBackground: settingsToUpdate },
      });
    },
    [updateProjectSettings]
  );
Option 2: Remove async/await if fire-and-forget is intentional
  const setBoardBackground = useCallback(
-   async (projectPath: string, imagePath: string | null) => {
+   (projectPath: string, imagePath: string | null) => {
      const current = getCurrentSettings(projectPath);
      const toUpdate = {
        ...current,
        imagePath,
        imageVersion: imagePath ? Date.now() : undefined,
      };
      store.setBoardBackground(projectPath, imagePath);
-     await persistSettings(projectPath, toUpdate);
+     persistSettings(projectPath, toUpdate);
    },
    [store, persistSettings, getCurrentSettings]
  );

Also applies to: 47-66


143-160: Consider extracting default settings to avoid duplication.

The default values in clearBoardBackground (lines 147-157) duplicate those in getCurrentSettings (lines 31-40). Extracting to a shared constant would ensure consistency.

Suggested refactor
+const DEFAULT_BACKGROUND_SETTINGS = {
+  imagePath: null,
+  imageVersion: undefined,
+  cardOpacity: 100,
+  columnOpacity: 100,
+  columnBorderEnabled: true,
+  cardGlassmorphism: true,
+  cardBorderEnabled: true,
+  cardBorderOpacity: 100,
+  hideScrollbar: false,
+};
+
 export function useBoardBackgroundSettings() {
   // ...
   const getCurrentSettings = useCallback(
     (projectPath: string) => {
       const current = store.boardBackgroundByProject[projectPath];
-      return (
-        current || {
-          imagePath: null,
-          cardOpacity: 100,
-          // ...
-        }
-      );
+      return current || DEFAULT_BACKGROUND_SETTINGS;
     },
     [store.boardBackgroundByProject]
   );
   // ...
   const clearBoardBackground = useCallback(
     async (projectPath: string) => {
       store.clearBoardBackground(projectPath);
-      await persistSettings(projectPath, {
-        imagePath: null,
-        // ...
-      });
+      await persistSettings(projectPath, DEFAULT_BACKGROUND_SETTINGS);
     },
     [store, persistSettings]
   );
apps/ui/src/lib/query-client.ts (1)

131-138: Consider deduplicating error handling to avoid double-logging.

The cache subscription calls handleQueryError for every query error, but individual queries using this client will also have their errors logged through the same handler if they don't provide their own onError. This could lead to duplicate logs for some error scenarios.

Additionally, queries that provide their own error handling may still trigger this global handler, potentially causing unexpected side effects like premature handleServerOffline() calls.

apps/ui/src/components/views/settings-view/hooks/use-cursor-status.ts (1)

47-49: The useCallback wrapper around refetch is unnecessary.

refetch from React Query is already a stable function reference. Wrapping it in useCallback adds indirection without benefit.

♻️ Suggested simplification
-  const loadData = useCallback(() => {
-    refetch();
-  }, [refetch]);
+  const loadData = refetch;

Alternatively, if you prefer keeping the same return semantics (void instead of Promise):

-  const loadData = useCallback(() => {
-    refetch();
-  }, [refetch]);
+  const loadData = useCallback(() => void refetch(), [refetch]);
apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-subagents.ts (1)

63-68: Redundant refetch() call after invalidateQueries().

invalidateQueries() already marks the query as stale and triggers a background refetch if there are active observers. Calling refetch() immediately after creates a duplicate fetch.

♻️ Suggested simplification
   const refreshFilesystemAgents = useCallback(async () => {
     await queryClient.invalidateQueries({
       queryKey: queryKeys.settings.agents(currentProject?.path ?? ''),
     });
-    await refetch();
   }, [queryClient, currentProject?.path, refetch]);
+  }, [queryClient, currentProject?.path]);
apps/ui/src/hooks/mutations/use-ideation-mutations.ts (1)

10-10: Remove unused toast import.

The toast import is not used in this file. The comment on line 80 confirms that toast notifications are handled by the component.

🧹 Suggested fix
 import { getElectronAPI } from '@/lib/electron';
 import { queryKeys } from '@/lib/query-keys';
-import { toast } from 'sonner';
 import type { IdeaCategory, IdeaSuggestion } from '@automaker/types';
apps/ui/src/hooks/queries/use-ideation.ts (1)

47-62: Consider cache key implications when query is disabled.

When projectPath is undefined, the queryKey uses an empty string (queryKeys.ideation.ideas('')). While the query is disabled, this creates a cache entry with an empty path. If multiple disabled queries exist, they share the same cache key, which could cause unexpected cache behavior when one gets enabled.

A common alternative is to only compute the queryKey when enabled, or use a sentinel value like '__disabled__' to make it explicit.

💡 Alternative pattern
 export function useIdeas(projectPath: string | undefined) {
   return useQuery({
-    queryKey: queryKeys.ideation.ideas(projectPath ?? ''),
+    queryKey: queryKeys.ideation.ideas(projectPath!),
     queryFn: async () => {
       if (!projectPath) throw new Error('No project path');
       // ...
     },
     enabled: !!projectPath,
     staleTime: STALE_TIMES.FEATURES,
   });
 }

With enabled: false, the queryFn won't run, so the ! assertion is safe. However, the current approach also works correctly since the empty string key is effectively isolated by the disabled state.

apps/ui/src/hooks/use-guided-prompts.ts (1)

50-53: The handleRefetch wrapper may be unnecessary.

React Query's refetch() already returns a Promise. The wrapper converts the return type but doesn't change behavior. If consumers don't rely on the void return type, you could expose refetch directly.

However, if the interface contract (Promise<void>) is important for backward compatibility, this wrapper is appropriate.

💡 Simplified alternative (if void return is not required)
-  // Convert async refetch to match the expected interface
-  const handleRefetch = useCallback(async () => {
-    await refetch();
-  }, [refetch]);
-
   return {
     // ...
-    refetch: handleRefetch,
+    refetch,
     // ...
   };
apps/ui/src/components/views/settings-view/worktrees/worktrees-section.tsx (1)

162-175: Consider migrating checkbox settings to React Query mutations for consistency.

These checkbox handlers still use direct API calls with only console.error for failures. While functional, this is inconsistent with the React Query migration pattern used for init scripts. Users won't know if settings fail to persist.

This could be addressed in a follow-up to maintain consistency and provide better user feedback via toast notifications.

apps/ui/src/hooks/queries/use-models.ts (1)

60-94: The refresh parameter doesn't affect cache key, leading to confusing behavior.

Both useCodexModels and useOpencodeModels accept a refresh parameter that's passed to the API but not included in the query key. This means:

  1. useCodexModels(true) and useCodexModels(false) share the same cache
  2. The refresh value only matters on the first call or after cache invalidation

Consider one of these approaches:

Option 1: Include refresh in query key (if different cache entries are desired)
 export function useCodexModels(refresh = false) {
   return useQuery({
-    queryKey: queryKeys.models.codex(),
+    queryKey: [...queryKeys.models.codex(), { refresh }],
     queryFn: async (): Promise<CodexModel[]> => {
Option 2: Remove parameter, use queryClient.invalidateQueries for refresh (recommended)
-export function useCodexModels(refresh = false) {
+export function useCodexModels() {
   return useQuery({
     queryKey: queryKeys.models.codex(),
     queryFn: async (): Promise<CodexModel[]> => {
       const api = getElectronAPI();
-      const result = await api.codex.getModels(refresh);
+      const result = await api.codex.getModels(false);

Then use queryClient.invalidateQueries({ queryKey: queryKeys.models.codex() }) when a refresh is needed.

apps/ui/src/hooks/mutations/use-settings-mutations.ts (1)

69-114: Dual invocation pattern is clever but verify projectPath extraction in onSuccess.

The flexible call pattern supporting both useUpdateProjectSettings(projectPath) and useUpdateProjectSettings().mutate({ projectPath, settings }) is well-designed.

However, on Line 102, data.projectPath relies on the spread from Line 99. If the API result already has a projectPath property, it could shadow your added one. Consider being explicit:

🔧 Suggested improvement
-      return { ...result, projectPath: path };
+      return { result, projectPath: path };
     },
     onSuccess: (data) => {
-      const path = data.projectPath || projectPath;
+      const path = data.projectPath;
apps/ui/src/components/views/settings-view/providers/opencode-settings-tab.tsx (2)

65-74: Redundant refetch after invalidation.

invalidateQueries already triggers a refetch for active queries. The explicit await refetchCliStatus() on Line 72 causes a duplicate fetch. Consider removing it:

♻️ Suggested simplification
 const handleRefreshOpencodeCli = useCallback(async () => {
   await Promise.all([
     queryClient.invalidateQueries({ queryKey: queryKeys.cli.opencode() }),
     queryClient.invalidateQueries({ queryKey: queryKeys.models.opencodeProviders() }),
     queryClient.invalidateQueries({ queryKey: queryKeys.models.opencode() }),
   ]);
-  await refetchCliStatus();
   toast.success('OpenCode CLI refreshed');
-}, [queryClient, refetchCliStatus]);
+}, [queryClient]);

53-63: Type assertion on method masks type safety without validating actual values.

Line 58 asserts cliStatusData.auth.method (which the API types as a generic string) to OpencodeAuthStatus['method'], a union of five specific literals. The fallback || 'none' only catches falsy values—if the API returns an unexpected string value, the assertion silently passes:

method: (cliStatusData.auth.method as OpencodeAuthStatus['method']) || 'none',

Consider validating the method against known values ('api_key' | 'api_key_env' | 'oauth' | 'config_file') before using it, or ensure the upstream API type definition guarantees only valid values are returned.

apps/ui/src/components/views/spec-view/hooks/use-spec-save.ts (1)

12-18: async keyword is misleading since mutation is fire-and-forget.

The saveSpec function is declared async but mutation.mutate() doesn't return a promise. This makes await saveSpec() from callers return immediately without waiting for the save to complete.

If callers need to await completion, use mutateAsync instead:

♻️ Option 1: Use mutateAsync for awaitable behavior
 const saveSpec = async () => {
   if (!currentProject) return;

-  saveMutation.mutate(appSpec, {
-    onSuccess: () => setHasChanges(false),
-  });
+  await saveMutation.mutateAsync(appSpec);
+  setHasChanges(false);
 };
♻️ Option 2: Remove async if fire-and-forget is intended
-const saveSpec = async () => {
+const saveSpec = () => {
   if (!currentProject) return;

   saveMutation.mutate(appSpec, {
     onSuccess: () => setHasChanges(false),
   });
 };
apps/ui/src/components/views/settings-view/providers/claude-settings-tab/hooks/use-skills-settings.ts (1)

20-48: Consider adding error handling for mutations.

Both updateEnabled and updateSources only handle onSuccess. If the mutation fails, users won't receive feedback. Consider adding onError callbacks to show error toasts.

Proposed improvement
     updateSettingsMutation.mutate(
       { enableSkills: newEnabled },
       {
         onSuccess: () => {
           useAppStore.setState({ enableSkills: newEnabled });
           toast.success(newEnabled ? 'Skills enabled' : 'Skills disabled');
         },
+        onError: (error) => {
+          toast.error('Failed to update skills setting', {
+            description: error instanceof Error ? error.message : 'Unknown error',
+          });
+        },
       }
     );

Apply the same pattern to updateSources.

apps/ui/src/hooks/queries/use-pipeline.ts (1)

24-38: Consider using a sentinel or conditional query key to avoid cache collisions.

When projectPath is undefined, the query key becomes queryKeys.pipeline.config(''). If multiple unrelated components call this hook without a path, they would share the same cache entry (keyed by empty string), potentially leading to stale or incorrect data when a valid path is later provided.

Since enabled: !!projectPath prevents the query from executing when projectPath is falsy, the throw on line 28 is effectively unreachable during normal operation—but the query key is still registered.

Suggested improvement
 export function usePipelineConfig(projectPath: string | undefined) {
   return useQuery({
-    queryKey: queryKeys.pipeline.config(projectPath ?? ''),
+    queryKey: queryKeys.pipeline.config(projectPath!),
     queryFn: async (): Promise<PipelineConfig | null> => {
-      if (!projectPath) throw new Error('No project path');
       const api = getHttpApiClient();
       const result = await api.pipeline.getConfig(projectPath);
       if (!result.success) {
         throw new Error(result.error || 'Failed to fetch pipeline config');
       }
       return result.config ?? null;
     },
     enabled: !!projectPath,
     staleTime: STALE_TIMES.SETTINGS,
   });
 }

The non-null assertion (!) is safe here because enabled: !!projectPath guarantees projectPath is truthy when queryFn executes. This also removes the redundant throw statement.

apps/ui/src/hooks/queries/use-usage.ts (1)

59-77: Consider extracting shared query logic.

Both useClaudeUsage and useCodexUsage are nearly identical. While the current approach is acceptable for clarity, you could reduce duplication with a factory function if more usage hooks are added in the future.

♻️ Optional: Factory pattern for usage hooks
function createUsageHook<T>(
  queryKeyFn: () => readonly string[],
  apiFn: () => Promise<T | { error: string; message?: string }>
) {
  return function useUsage(enabled = true) {
    return useQuery({
      queryKey: queryKeyFn(),
      queryFn: async (): Promise<T> => {
        const result = await apiFn();
        if ('error' in result) {
          throw new Error(result.message || result.error);
        }
        return result;
      },
      enabled,
      staleTime: STALE_TIMES.USAGE,
      refetchInterval: enabled ? USAGE_POLLING_INTERVAL : false,
      placeholderData: (previousData) => previousData,
    });
  };
}
apps/ui/src/hooks/queries/use-git.ts (1)

19-36: Verify empty-string query key behavior.

The hook uses an empty string fallback for queryKey when projectPath is undefined (line 21). While the query is disabled in this case (enabled: !!projectPath && enabled), this pattern could potentially cause cache collisions if multiple disabled queries share the same ['git', 'diffs', ''] key.

Consider returning early or using a more defensive pattern:

♻️ Alternative: Skip query when path is missing
 export function useGitDiffs(projectPath: string | undefined, enabled = true) {
   return useQuery({
-    queryKey: queryKeys.git.diffs(projectPath ?? ''),
+    queryKey: queryKeys.git.diffs(projectPath!),
     queryFn: async () => {
-      if (!projectPath) throw new Error('No project path');
       const api = getElectronAPI();
       const result = await api.git.getDiffs(projectPath);
       if (!result.success) {
         throw new Error(result.error || 'Failed to fetch diffs');
       }
       return {
         files: result.files ?? [],
         diff: result.diff ?? '',
       };
     },
     enabled: !!projectPath && enabled,
     staleTime: STALE_TIMES.WORKTREES,
   });
 }

The non-null assertion is safe here because queryFn only runs when enabled is true, which requires projectPath to be truthy.

apps/ui/src/components/views/board-view/hooks/use-board-actions.ts (2)

485-491: Consider removing async keyword from handleVerifyFeature.

The function no longer awaits any promises since mutation.mutate() is fire-and-forget. The async keyword is now unnecessary.

♻️ Minor cleanup
 const handleVerifyFeature = useCallback(
-    async (feature: Feature) => {
+    (feature: Feature) => {
       if (!currentProject) return;
       verifyFeatureMutation.mutate(feature.id);
     },
     [currentProject, verifyFeatureMutation]
   );

493-503: Consider removing async keyword from handleResumeFeature.

Same as above—the function no longer awaits any promises.

♻️ Minor cleanup
 const handleResumeFeature = useCallback(
-    async (feature: Feature) => {
+    (feature: Feature) => {
       logger.info('handleResumeFeature called for feature:', feature.id);
       if (!currentProject) {
         logger.error('No current project');
         return;
       }
       resumeFeatureMutation.mutate({ featureId: feature.id, useWorktrees });
     },
     [currentProject, resumeFeatureMutation, useWorktrees]
   );
apps/ui/src/components/views/spec-view/hooks/use-spec-generation.ts (1)

414-441: Consider adding onSuccess callback for resilience.

The mutation relies entirely on the WebSocket event subscription (lines 216-400) for success handling. If the WebSocket connection is interrupted or the event is missed, the UI state could become stale. Consider adding an onSuccess callback as a fallback to ensure the UI reflects the operation result.

Suggested addition
     createSpecMutation.mutate(
       {
         projectOverview: projectOverview.trim(),
         generateFeatures,
         analyzeProject: analyzeProjectOnCreate,
         featureCount: generateFeatures ? featureCountOnCreate : undefined,
       },
       {
+        onSuccess: () => {
+          // Events handle the detailed state updates, but this ensures
+          // we don't get stuck if WebSocket events are missed
+          logger.debug('[useSpecGeneration] Spec creation request sent successfully');
+        },
         onError: (error) => {
           // ... existing error handling
         },
       }
     );
apps/ui/src/hooks/queries/use-workspace.ts (1)

12-15: Consider exporting WorkspaceDirectory interface.

The interface is duplicated in workspace-picker-modal.tsx (lines 13-16). Consider exporting it from this file to maintain a single source of truth.

Suggested change
-interface WorkspaceDirectory {
+export interface WorkspaceDirectory {
   name: string;
   path: string;
 }

Then in workspace-picker-modal.tsx:

-interface WorkspaceDirectory {
-  name: string;
-  path: string;
-}
+import { useWorkspaceDirectories, type WorkspaceDirectory } from '@/hooks/queries';
apps/ui/src/components/views/board-view/worktree-panel/hooks/use-branches.ts (1)

30-41: Good refetch logic, but refetch() result is ignored.

When calling refetch() for the same path, the returned promise is discarded. If consumers expect fetchBranches to be awaitable or want to handle refetch errors, this could be a limitation.

Consider whether the interface should remain fire-and-forget or if error propagation is needed.

apps/ui/src/components/codex-usage-popover.tsx (1)

228-236: Consider disabling the refresh button during fetch.

The button checks !isFetching before calling refetch(), but visually it's not disabled. Users may click multiple times expecting action. Consider adding disabled={isFetching} for better UX.

Suggested improvement
           <Button
             variant="ghost"
             size="icon"
-            className={cn('h-6 w-6', isFetching && 'opacity-80')}
+            className={cn('h-6 w-6', isFetching && 'opacity-80')}
+            disabled={isFetching}
             onClick={() => !isFetching && refetch()}
           >
apps/ui/src/components/views/settings-view/hooks/use-cursor-permissions.ts (2)

1-3: Remove unused useEffect import.

useEffect is imported but not used in this file.

Suggested fix
-import { useState, useCallback, useEffect } from 'react';
+import { useState, useCallback } from 'react';

29-39: Consider cleanup for the timeout.

The setTimeout for resetting copiedConfig works, but if the component unmounts before the timeout fires, it could cause a React state update warning. This is a minor edge case.

Optional: Add cleanup with useEffect
useEffect(() => {
  if (!copiedConfig) return;
  const timer = setTimeout(() => setCopiedConfig(false), 2000);
  return () => clearTimeout(timer);
}, [copiedConfig]);

const copyConfig = useCallback(
  (profileId: 'strict' | 'development') => {
    copyConfigMutation.mutate(profileId, {
      onSuccess: () => setCopiedConfig(true),
    });
  },
  [copyConfigMutation]
);
apps/ui/src/components/views/running-agents-view.tsx (1)

24-41: Stop button disables for all agents when any stop is pending.

When stopFeature.mutate() is called, stopFeature.isPending becomes true and disables the Stop button for all running agents (line 172), not just the one being stopped. This may confuse users who want to stop multiple agents quickly.

Consider tracking pending state per feature ID:

const [pendingStops, setPendingStops] = useState<Set<string>>(new Set());

const handleStopAgent = useCallback(
  (featureId: string) => {
    setPendingStops(prev => new Set(prev).add(featureId));
    stopFeature.mutate(featureId, {
      onSettled: () => {
        setPendingStops(prev => {
          const next = new Set(prev);
          next.delete(featureId);
          return next;
        });
      },
    });
  },
  [stopFeature]
);

Then use disabled={pendingStops.has(agent.featureId)} on the button.

apps/ui/src/components/ui/git-diff-panel.tsx (1)

380-387: Consider using a type guard for cleaner error handling.

The error handling logic could be simplified:

♻️ Optional refactor
-  const error = queryError
-    ? queryError instanceof Error
-      ? queryError.message
-      : 'Failed to load diffs'
-    : null;
+  const error = queryError 
+    ? (queryError instanceof Error ? queryError.message : 'Failed to load diffs')
+    : null;

Or extract a utility function if this pattern is repeated elsewhere in the codebase.

apps/ui/src/components/usage-popover.tsx (1)

27-28: Unused constant REFRESH_INTERVAL_SECONDS.

This constant is defined but never used in the component. Polling is now handled by React Query's configuration in the hooks. Consider removing it to avoid confusion.

♻️ Proposed fix
-// Fixed refresh interval (45 seconds)
-const REFRESH_INTERVAL_SECONDS = 45;
apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx (1)

100-113: Consider migrating polling to React Query's refetchInterval.

The manual setInterval for worktree polling works, but React Query can handle this natively via refetchInterval. This would provide automatic cleanup and consistency with the init script hook's approach.

♻️ Suggested approach

When migrating useWorktrees to React Query, configure it with:

useQuery({
  queryKey: queryKeys.worktrees.list(projectPath),
  queryFn: fetchWorktreesFromAPI,
  refetchInterval: 5000,
});

This eliminates manual interval management and ensures proper cleanup.

apps/ui/src/hooks/queries/use-running-agents.ts (1)

55-61: Consider using select for derived data.

The current pattern works due to React Query's deduplication, but using the select option provides a cleaner derivation with proper memoization:

♻️ Suggested refactor using select
 export function useRunningAgentsCount() {
-  const query = useRunningAgents();
-  return {
-    ...query,
-    data: query.data?.count ?? 0,
-  };
+  return useQuery({
+    queryKey: queryKeys.runningAgents.all(),
+    queryFn: async (): Promise<RunningAgentsResult> => {
+      const api = getElectronAPI();
+      const result = await api.runningAgents.getAll();
+      if (!result.success) {
+        throw new Error(result.error || 'Failed to fetch running agents');
+      }
+      return {
+        agents: result.runningAgents ?? [],
+        count: result.totalCount ?? 0,
+      };
+    },
+    staleTime: STALE_TIMES.RUNNING_AGENTS,
+    select: (data) => data.count,
+  });
 }

Alternatively, extract the queryFn to avoid duplication.

apps/ui/src/components/claude-usage-popover.tsx (1)

167-176: Add disabled attribute for better accessibility.

The button guards clicks with !isFetching but doesn't set disabled, which can confuse assistive technologies and users.

♻️ Suggested fix
             <Button
               variant="ghost"
               size="icon"
               className={cn('h-6 w-6', isFetching && 'opacity-80')}
               onClick={() => !isFetching && refetch()}
+              disabled={isFetching}
             >
               <RefreshCw className={cn('w-3.5 h-3.5', isFetching && 'animate-spin')} />
             </Button>
apps/ui/src/hooks/queries/use-spec.ts (1)

77-103: Good polling pattern with enabled toggle.

The hook correctly uses refetchInterval: enabled ? 5000 : false to enable/disable polling based on context. The graceful fallback for missing api.specRegeneration is defensive.

Minor: Consider extracting the hardcoded 5000 to a constant (e.g., STALE_TIMES.REGENERATION_STATUS) for consistency with other hooks.

apps/ui/src/hooks/queries/use-settings.ts (1)

66-76: Inconsistent error handling compared to other hooks.

useSettingsStatus returns the raw API result without checking result.success, while all other hooks in this file throw on failure. This inconsistency may confuse consumers expecting uniform behavior.

♻️ Suggested fix for consistency
 export function useSettingsStatus() {
   return useQuery({
     queryKey: queryKeys.settings.status(),
     queryFn: async () => {
       const api = getElectronAPI();
       const result = await api.settings.getStatus();
+      if (!result.success) {
+        throw new Error(result.error || 'Failed to fetch settings status');
+      }
       return result;
     },
     staleTime: STALE_TIMES.SETTINGS,
   });
 }
apps/ui/src/components/views/board-view/hooks/use-board-features.ts (1)

27-32: Unused refetch from useFeatures.

The refetch function is destructured and renamed to loadFeatures on line 31, but it's never used. Instead, loadFeatures is redefined in the return object (lines 164-168) to call invalidateQueries. While both trigger a refetch, this is confusing.

♻️ Simplify by removing unused destructuring
   // Use React Query for features
   const {
     data: features = [],
     isLoading,
-    refetch: loadFeatures,
   } = useFeatures(currentProject?.path);

Or use the refetch directly:

   return {
     features,
     isLoading,
     persistedCategories,
-    loadFeatures: () => {
-      queryClient.invalidateQueries({
-        queryKey: queryKeys.features.all(currentProject?.path ?? ''),
-      });
-    },
+    loadFeatures: refetch,
     loadCategories,
     saveCategory,
   };

Also applies to: 164-168

apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts (1)

19-28: Consider removing redundant async keyword.

The handler functions are declared async but don't use await. Since mutation.mutate() is synchronous (fire-and-forget), the async keyword is unnecessary and could mislead readers into thinking these operations are awaited.

Suggested change
  const handleSwitchBranch = useCallback(
-    async (worktree: WorktreeInfo, branchName: string) => {
+    (worktree: WorktreeInfo, branchName: string) => {
      if (switchBranchMutation.isPending || branchName === worktree.branch) return;
      switchBranchMutation.mutate({
        worktreePath: worktree.path,
        branchName,
      });
    },
    [switchBranchMutation]
  );

Apply the same pattern to handlePull, handlePush, and handleOpenInEditor.

apps/ui/src/hooks/mutations/use-github-mutations.ts (2)

47-48: Remove unused queryClient declaration.

queryClient is declared but never used in useValidateIssue. The comment at lines 91-92 explains cache invalidation happens via WebSocket events, so this declaration is dead code.

Suggested fix
 export function useValidateIssue(projectPath: string) {
-  const queryClient = useQueryClient();
-
   return useMutation({

136-158: Consider refactoring useGetValidationStatus to a query hook.

This hook only fetches data without side effects, making it semantically a query rather than a mutation. Using useMutation for read-only operations loses React Query benefits like automatic caching, stale-while-revalidate, and background refetching.

Suggested refactor to useQuery
export function useValidationStatus(projectPath: string, enabled = true) {
  return useQuery({
    queryKey: queryKeys.github.validationStatus(projectPath),
    queryFn: async () => {
      const api = getElectronAPI();
      if (!api.github?.getValidationStatus) {
        throw new Error('Validation status API not available');
      }
      const result = await api.github.getValidationStatus(projectPath);
      if (!result.success) {
        throw new Error(result.error || 'Failed to get validation status');
      }
      return result.runningIssues ?? [];
    },
    enabled,
    staleTime: STALE_TIMES.RUNNING_AGENTS, // Running validations change frequently
  });
}
apps/ui/src/hooks/mutations/use-worktree-mutations.ts (5)

84-107: Consider accepting projectPath for consistent cache invalidation.

The generic ['worktrees'] invalidation is overly broad compared to other hooks that use queryKeys.worktrees.all(projectPath). This will invalidate worktree caches for all projects, not just the affected one.

Consider either:

  1. Accept projectPath as a hook parameter (like useCreateWorktree)
  2. Accept projectPath in the mutation payload alongside worktreePath

This would enable scoped invalidation: queryKeys.worktrees.all(projectPath).

♻️ Example refactor
-export function useCommitWorktree() {
+export function useCommitWorktree(projectPath: string) {
   const queryClient = useQueryClient();
 
   return useMutation({
     mutationFn: async ({ worktreePath, message }: { worktreePath: string; message: string }) => {
       const api = getElectronAPI();
       const result = await api.worktree.commit(worktreePath, message);
       if (!result.success) {
         throw new Error(result.error || 'Failed to commit changes');
       }
       return result.result;
     },
-    onSuccess: (_, { worktreePath }) => {
-      // Invalidate all worktree queries since we don't know the project path
-      queryClient.invalidateQueries({ queryKey: ['worktrees'] });
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: queryKeys.worktrees.all(projectPath) });
       toast.success('Changes committed');
     },

114-165: Same broad invalidation pattern.

Same suggestion applies here — consider accepting projectPath as a hook parameter for usePushWorktree and usePullWorktree to enable scoped cache invalidation.


172-221: Good conditional handling, but could leverage projectPath for scoped invalidation.

The toast logic handles different PR creation outcomes well. The options?.projectPath is available in the payload — consider using it (when present) for more targeted invalidation:

 onSuccess: (result, { options }) => {
-  queryClient.invalidateQueries({ queryKey: ['worktrees'] });
+  if (options?.projectPath) {
+    queryClient.invalidateQueries({ queryKey: queryKeys.worktrees.all(options.projectPath) });
+  } else {
+    queryClient.invalidateQueries({ queryKey: ['worktrees'] });
+  }
   queryClient.invalidateQueries({ queryKey: ['github', 'prs'] });

275-338: Same broad invalidation pattern as other hooks.

Consider the same refactor for useSwitchBranch and useCheckoutBranch to accept projectPath for scoped invalidation.


397-420: Consider using projectPath for scoped invalidation.

projectPath is available in the mutation function. Consider using it for more targeted cache invalidation, or alternatively accept it as a hook parameter for consistency with other hooks.

apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts (1)

31-36: Potential stale store data when worktrees list becomes empty.

The condition worktrees.length > 0 prevents syncing an empty array to the store. While this avoids clearing during initial load (when data is undefined), it also means if all worktrees are legitimately removed, the store retains stale data.

Consider checking data existence instead:

Suggested improvement
   // Sync worktrees to Zustand store when they change
   useEffect(() => {
-    if (worktrees.length > 0) {
+    if (data) {
       setWorktreesInStore(projectPath, worktrees);
     }
-  }, [worktrees, projectPath, setWorktreesInStore]);
+  }, [data, worktrees, projectPath, setWorktreesInStore]);

@Shironex Shironex added Testers-Requested Request for others to test an enhancement or bug fix/etc. Work-In-Progress Currently being addressed. Do Not Merge Use this label if something should not be merged. labels Jan 15, 2026
Shironex and others added 2 commits January 15, 2026 19:10
- Add cache invalidation to useBoardPersistence after create/update/delete
- Add useAutoModeQueryInvalidation to board-view for WebSocket events
- Add cache invalidation to github-issues-view after converting issue to task
- Add cache invalidation to analysis-view after generating features
- Fix UI not updating when features are added, updated, or completed

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Update query keys to include all relevant parameters (branches, agents)
- Fix use-branches to pass includeRemote parameter to query key
- Fix use-settings to include sources in agents query key
- Update running-agents-view to use correct query key structure
- Update use-spec-loading to properly use spec query hooks
- Add missing queryClient invalidation in auto-mode mutations
- Add missing cache invalidation in spec mutations after creation

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@Shironex Shironex self-assigned this Jan 15, 2026
Base automatically changed from v0.12.0rc to main January 17, 2026 23:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Do Not Merge Use this label if something should not be merged. Testers-Requested Request for others to test an enhancement or bug fix/etc. Work-In-Progress Currently being addressed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants