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

  • New Features

    • Centralized query-driven data fetching and mutation with in-app devtools; shared loading skeleton.
  • Bug Fixes & Improvements

    • Unified error messages, refresh and retry behavior across usage, worktrees, sessions, GitHub, ideation, and settings.
    • Widespread memoization to reduce UI flicker and improve consistency.
  • Performance

    • Virtualized rendering for boards and chat; optimized graph rendering for large projects.

✏️ 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 <noreply@anthropic.com>
- Add useFeatures, useFeature, useAgentOutput for feature data
- Add useGitHubIssues, useGitHubPRs, useGitHubValidations, useGitHubIssueComments
- Add useClaudeUsage, useCodexUsage with polling intervals
- Add useRunningAgents, useRunningAgentsCount
- Add useWorktrees, useWorktreeInfo, useWorktreeStatus, useWorktreeDiffs
- Add useGlobalSettings, useProjectSettings, useCredentials
- Add useAvailableModels, useCodexModels, useOpencodeModels
- Add useSessions, useSessionHistory, useSessionQueue
- Add useIdeationPrompts, useIdeas
- Add CLI status queries (claude, cursor, codex, opencode, github)
- Add useCursorPermissionsQuery, useWorkspaceDirectories
- Add usePipelineConfig, useSpecFile, useSpecRegenerationStatus

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Jan 15, 2026

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

Replaces many imperative Electron/API calls and local state with centralized React Query hooks and mutations, adds a configured QueryClient + query-keys, introduces extensive UI virtualization and memoization, adds many query/mutation barrels and invalidation hooks, and adds dependency-resolver helpers and related tests.

Changes

Cohort / File(s) Summary
Package / Query infra
apps/ui/package.json, apps/ui/src/lib/query-client.ts, apps/ui/src/lib/query-keys.ts, apps/ui/src/routes/__root.tsx
Bumped React Query, added devtools, created QueryClient with domain STALE/GC times and global handlers, centralized queryKeys, and wrapped app with QueryClientProvider (+ optional devtools).
Queries: new hooks & barrel
apps/ui/src/hooks/queries/*, apps/ui/src/hooks/queries/index.ts
Added many React Query hooks for features, worktrees, git, GitHub, sessions, usage, models, CLI status, ideation, spec, pipeline, cursor permissions, workspace, running agents, and a queries barrel.
Mutations: new hooks & barrel
apps/ui/src/hooks/mutations/*, apps/ui/src/hooks/mutations/index.ts
Added mutation hooks for features, auto-mode, worktrees, GitHub, settings, spec, ideation, cursor permissions and a mutations barrel export.
Usage UI
apps/ui/src/components/claude-usage-popover.tsx, apps/ui/src/components/codex-usage-popover.tsx, apps/ui/src/components/usage-popover.tsx, apps/ui/src/components/views/settings-view/**/codex-usage-section.tsx, claude-usage sections
Replaced manual Electron fetch/interval logic with useClaudeUsage/useCodexUsage hooks; moved to hook-driven isLoading/isFetching/error; wired refetch for manual refresh; simplified error messages.
Workspaces / Sessions / Diffs
apps/ui/src/components/dialogs/workspace-picker-modal.tsx, apps/ui/src/components/session-manager.tsx, apps/ui/src/components/ui/git-diff-panel.tsx, apps/ui/src/hooks/queries/use-workspace.ts
Switched to useWorkspaceDirectories/useSessions/useGitDiffs/useWorktreeDiffs hooks; removed local load effects; added path to WorkspaceDirectory interface.
Board / Kanban & Worktree actions
apps/ui/src/components/views/board-view/**/*, apps/ui/src/components/views/board-view/worktree-panel/hooks/*, apps/ui/src/hooks/mutations/use-worktree-mutations.ts
Replaced local API/store flows with queries/mutations (useFeatures, usePipelineConfig, worktree queries/mutations); added useAutoModeQueryInvalidation; changed useWorktreeActions signature (no args); passed projectPath into AgentInfoPanel; added reduceEffects prop to KanbanCard.
Virtualization & Memoization
apps/ui/src/components/views/board-view/kanban-board.tsx, apps/ui/src/components/views/chat-history.tsx, apps/ui/src/components/views/board-view/components/kanban-column.tsx, apps/ui/src/components/views/board-view/components/kanban-card/*
Added virtualization layers for Kanban columns and chat history, item measurement/offset logic, new KanbanColumn contentRef/onScroll props and spacing control; memoized multiple card components and introduced shallow selectors to reduce re-renders.
Graph performance & compact mode
apps/ui/src/components/views/graph-view/*, apps/ui/src/components/views/graph-view/hooks/*, apps/ui/src/components/views/graph-view/constants.ts
Added GRAPH_RENDER_MODE_FULL/COMPACT and thresholds, detect large graphs to switch to compact mode, propagate renderMode to nodes/edges, and refactored graph hooks to use precomputed maps and Sets for performance.
Spec / Ideation / GitHub / Running Agents
apps/ui/src/components/views/spec-view/*, apps/ui/src/components/views/ideation-view/*, apps/ui/src/components/views/github-*/**, apps/ui/src/components/views/running-agents-view.tsx
Converted spec/ideation/GitHub/running-agents flows to queries/mutations; added spec and feature generation mutations; updated UIs to rely on query/mutation states and refetch/invalidation.
Settings & CLI status
apps/ui/src/components/views/settings-view/**, apps/ui/src/components/ui/skeleton.tsx, apps/ui/src/hooks/queries/use-cli-status.ts
Centralized SkeletonPulse component; replaced local CLI checks with query hooks; converted multiple settings update flows to use mutations and query-driven data.
Dependency resolver helpers & tests
libs/dependency-resolver/src/resolver.ts, libs/dependency-resolver/src/index.ts, libs/dependency-resolver/tests/resolver.test.ts
Added createFeatureMap and getBlockingDependenciesFromMap helpers and exported them; added tests to validate parity with existing dependency logic.
Query invalidation & utilities
apps/ui/src/hooks/use-query-invalidation.ts, apps/ui/src/lib/query-keys.ts
Added hooks to invalidate queries on WebSocket/Electron events and centralized typed query-keys factory used across hooks/mutations.
Misc / migration / docs
apps/ui/src/hooks/use-settings-migration.ts, TODO.md
Removed eventHooks from settings hydration/update payloads; added a "Performance (completed)" section to TODO.

Sequence Diagram(s)

(Skipped — changes span many distinct features and flows; no single focused multi-component flow was singled out for visualization.)

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • #203 — Claude usage UI/API changes: directly related to migrating Claude usage components to useClaudeUsage and updated settings/usage UI.
  • #291 — Pipeline feature & UI integration: related to usePipelineConfig additions and pipeline cache invalidation introduced here.
  • #405 — Query/mutation infra and barrel exports: closely related to the new query/mutation barrels, query-client setup, and widespread refactors to hook-based data flows.

Poem

🐰 I hopped through queries, seeds in paw,

Caching carrots, tidy and raw,
Mutations munch, invalidations sing,
Boards now skim like birds in spring,
A nose twitch, a toast — devs, take a bow! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.64% 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 title clearly and concisely summarizes the main change: migrating to React Query for data fetching, which is the primary objective of the entire changeset.

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

✨ Finishing touches
  • 📝 Generate docstrings

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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
@Shironex Shironex self-assigned this Jan 15, 2026
Base automatically changed from v0.12.0rc to main January 17, 2026 23:50
@Shironex Shironex changed the base branch from main to v0.13.0rc January 19, 2026 12:15
Merged latest changes from v0.13.0rc into feat/react-query while preserving
React Query migration. Key merge decisions:

- Kept React Query hooks for data fetching (useRunningAgents, useStopFeature, etc.)
- Added backlog plan handling to running-agents-view stop functionality
- Imported both SkeletonPulse and Spinner for CLI status components
- Used Spinner for refresh buttons across all settings sections
- Preserved isBacklogPlan check in agent-output-modal TaskProgressPanel
- Added handleOpenInIntegratedTerminal to worktree actions while keeping React Query mutations
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: 3

Caution

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

⚠️ Outside diff range comments (2)
apps/ui/src/components/views/github-prs-view.tsx (1)

317-320: group-hover won’t trigger without a group parent.

The external-link button uses group-hover:opacity-100, but the row container lacks group, so the hover state may never activate. Consider adding group to the row wrapper.

✅ Minimal fix
-      className={cn(
-        'flex items-start gap-3 p-3 cursor-pointer hover:bg-accent/50 transition-colors',
+      className={cn(
+        'group flex items-start gap-3 p-3 cursor-pointer hover:bg-accent/50 transition-colors',
         isSelected && 'bg-accent'
       )}

Also applies to: 384-384

apps/ui/src/components/dialogs/workspace-picker-modal.tsx (1)

27-79: Use isError from React Query instead of relying on error instanceof Error.

The current error handling depends on error being an Error instance. While the hook implementation throws Error objects, using the explicit isError boolean flag is more defensive and avoids false negatives where the UI falls through to the empty state instead of showing an error message. Add isError to the destructuring and use it to branch error UI, along with a safe fallback message for non-Error failures.

Suggested fix
-  const { data: directories = [], isLoading, error, refetch } = useWorkspaceDirectories(open);
+  const { data: directories = [], isLoading, isError, error, refetch } = useWorkspaceDirectories(open);

-  const errorMessage = error instanceof Error ? error.message : null;
+  const errorMessage =
+    isError ? (error instanceof Error ? error.message : 'Failed to load projects') : null;

-          {errorMessage && !isLoading && (
+          {isError && !isLoading && (
             <div className="flex flex-col items-center justify-center h-full gap-3 text-center px-4">
               <div className="w-12 h-12 rounded-full bg-destructive/10 flex items-center justify-center">
                 <AlertCircle className="w-6 h-6 text-destructive" />
               </div>
               <p className="text-sm text-destructive">{errorMessage}</p>
               <Button variant="secondary" size="sm" onClick={() => refetch()} className="mt-2">
                 Try Again
               </Button>
             </div>
           )}
 
-          {!isLoading && !errorMessage && directories.length === 0 && (
+          {!isLoading && !isError && directories.length === 0 && (
             <div className="flex flex-col items-center justify-center h-full gap-3 text-center px-4">
               <div className="w-12 h-12 rounded-full bg-muted flex items-center justify-center">
                 <Folder className="w-6 h-6 text-muted-foreground" />
               </div>
               <p className="text-sm text-muted-foreground">
                 No projects found in workspace directory
               </p>
             </div>
           )}
 
-          {!isLoading && !errorMessage && directories.length > 0 && (
+          {!isLoading && !isError && directories.length > 0 && (
🤖 Fix all issues with AI agents
In `@apps/ui/src/components/claude-usage-popover.tsx`:
- Around line 20-32: The popover's authentication check (variable isCliVerified
in claude-usage-popover.tsx) is stricter than the other components—it's checking
claudeAuthStatus?.authenticated && claudeAuthStatus?.method ===
'cli_authenticated' before calling useClaudeUsage; to align behavior, remove the
method check and set isCliVerified to claudeAuthStatus?.authenticated (or, if
the stricter policy is desired, update claude-usage-section.tsx and
usage-popover.tsx to include the method check); update the isCliVerified
definition and any callers of useClaudeUsage accordingly so all three components
use the same authentication logic.

In `@apps/ui/src/components/views/ideation-view/components/prompt-list.tsx`:
- Line 63: The disabled state is inconsistent: the click-guard checks
generateMutation.isPending but the computed isDisabled (used for
styling/hover/cursor) does not; update the isDisabled calculation in the
prompt-list component to include generateMutation.isPending (alongside
loadingPromptId and generatingPromptIds.has(prompt.id)) so that when
generateMutation.isPending is true the card shows disabled styles and pointer
events are prevented for the prompt with prompt.id.

In
`@apps/ui/src/components/views/settings-view/providers/opencode-settings-tab.tsx`:
- Around line 65-74: The refresh flow in handleRefreshOpencodeCli needs error
handling: wrap the invalidateQueries + refetchCliStatus calls in a try/catch,
await the Promise.all and then await refetchCliStatus inside the try block, call
toast.success('OpenCode CLI refreshed') only on success, and in the catch block
call toast.error(...) with the caught error message (or formatted error) so
failures to invalidate or refetch show an error toast; reference the
handleRefreshOpencodeCli function, queryClient.invalidateQueries,
refetchCliStatus, toast.success and toast.error when making the change.
♻️ Duplicate comments (1)
apps/ui/src/components/views/board-view.tsx (1)

1409-1414: Dual source of truth is acceptable for gradual migration.

The cache invalidation followed by Zustand update for backward compatibility is a pragmatic approach during migration. Consider tracking this as tech debt to eventually consolidate on React Query as the single source of truth once all consumers are migrated.

🧹 Nitpick comments (17)
apps/ui/src/components/ui/git-diff-panel.tsx (1)

355-390: Well-implemented React Query migration with correct hook patterns.

The implementation correctly:

  • Calls both hooks unconditionally (respecting Rules of Hooks)
  • Uses parameter-based conditional enabling (projectPath: undefined disables the query)
  • Derives state from the appropriate hook based on useWorktrees
  • Extracts files, diffContent, and error from the selected hook's data

One minor observation: loadDiffs (the refetch function) is passed directly to onClick handlers. While this works, the refetch function will receive the click event as an argument (which it ignores). This is a common pattern but could be made more explicit.

💡 Optional: More explicit click handler (nitpick)
-  const loadDiffs = useWorktrees ? refetchWorktree : refetchGit;
+  const loadDiffs = () => {
+    if (useWorktrees) {
+      refetchWorktree();
+    } else {
+      refetchGit();
+    }
+  };
apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktree-actions.ts (1)

68-76: Missing isPending guard.

Unlike handleSwitchBranch, handlePull, and handlePush, this handler doesn't check openInEditorMutation.isPending before calling mutate. Rapid clicks could trigger multiple editor open attempts.

♻️ Add isPending guard for consistency
   const handleOpenInEditor = useCallback(
     async (worktree: WorktreeInfo, editorCommand?: string) => {
+      if (openInEditorMutation.isPending) return;
       openInEditorMutation.mutate({
         worktreePath: worktree.path,
         editorCommand,
       });
     },
     [openInEditorMutation]
   );
apps/ui/src/components/views/running-agents-view.tsx (3)

26-26: Move logger creation outside the component or memoize it.

Creating logger inside the component causes it to be recreated on every render. Since it's included in the dependency array of handleStopAgent (line 54), this triggers unnecessary callback recreation.

♻️ Suggested fix
+const logger = createLogger('RunningAgentsView');
+
 export function RunningAgentsView() {
   const [selectedAgent, setSelectedAgent] = useState<RunningAgent | null>(null);
   const { setCurrentProject, projects } = useAppStore();
   const navigate = useNavigate();
-
-  const logger = createLogger('RunningAgentsView');

77-83: Missing logger in dependency array.

handleViewLogs uses logger but doesn't include it in the dependency array. This is a minor ESLint exhaustive-deps violation. If you move logger outside the component as suggested above, this becomes a non-issue since module-level constants don't need to be in deps.


199-207: Consider disabling Stop button per-agent during mutation.

The Stop button is disabled for all agents when any stopFeature mutation is pending. If a user wants to stop multiple agents quickly, they'd need to wait for each to complete. Consider tracking which agent is being stopped to only disable that specific button.

♻️ Per-agent pending state tracking
+const [stoppingAgentId, setStoppingAgentId] = useState<string | null>(null);

 const handleStopAgent = useCallback(
   async (agent: RunningAgent) => {
     const api = getElectronAPI();
     const isBacklogPlan = agent.featureId.startsWith('backlog-plan:');
     if (isBacklogPlan && api.backlogPlan) {
       logger.debug('Stopping backlog plan agent', { featureId: agent.featureId });
+      setStoppingAgentId(agent.featureId);
       await api.backlogPlan.stop();
+      setStoppingAgentId(null);
       refetch();
       return;
     }
+    setStoppingAgentId(agent.featureId);
-    stopFeature.mutate({ featureId: agent.featureId, projectPath: agent.projectPath });
+    stopFeature.mutate(
+      { featureId: agent.featureId, projectPath: agent.projectPath },
+      { onSettled: () => setStoppingAgentId(null) }
+    );
   },
   [stopFeature, refetch, logger]
 );

 // In the button:
-disabled={stopFeature.isPending}
+disabled={stoppingAgentId === agent.featureId}
apps/ui/src/components/views/board-view/components/kanban-card/agent-info-panel.tsx (1)

80-80: Consider extracting polling interval as a constant.

The 3000ms polling interval is duplicated. Extract it to a named constant for clarity and easier adjustment.

♻️ Extract polling constant
+const AGENT_INFO_POLL_INTERVAL = 3000;
+
 export function AgentInfoPanel({
   // ...
 }) {
   // ...
   const { data: freshFeature } = useFeature(projectPath, feature.id, {
     enabled: shouldFetchData && !contextContent,
-    pollingInterval: shouldPoll ? 3000 : false,
+    pollingInterval: shouldPoll ? AGENT_INFO_POLL_INTERVAL : false,
   });

   const { data: agentOutputContent } = useAgentOutput(projectPath, feature.id, {
     enabled: shouldFetchData && !contextContent,
-    pollingInterval: shouldPoll ? 3000 : false,
+    pollingInterval: shouldPoll ? AGENT_INFO_POLL_INTERVAL : false,
   });

Also applies to: 86-86

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

642-656: Consider error handling for partial failures in feature creation loop.

The loop creates multiple features sequentially but doesn't track individual failures. If some features fail to create, the cache is still invalidated and featureListGenerated is set to true, potentially showing a success message despite partial failures.

💡 Optional: Track partial failures
+      const errors: string[] = [];
       for (const detectedFeature of detectedFeatures) {
-        await api.features.create(currentProject.path, {
+        const result = await api.features.create(currentProject.path, {
           id: generateUUID(),
           category: detectedFeature.category,
           description: detectedFeature.description,
           status: 'backlog',
           steps: [],
         } as any);
+        if (!result.success) {
+          errors.push(result.error || `Failed to create ${detectedFeature.category}`);
+        }
       }

       // Invalidate React Query cache to sync UI
       queryClient.invalidateQueries({
         queryKey: queryKeys.features.all(currentProject.path),
       });

-      setFeatureListGenerated(true);
+      if (errors.length > 0) {
+        setFeatureListError(`Created ${detectedFeatures.length - errors.length}/${detectedFeatures.length} features`);
+      } else {
+        setFeatureListGenerated(true);
+      }
apps/ui/src/components/views/board-view/hooks/use-board-persistence.ts (1)

50-55: Consider potential redundancy between local state update and cache invalidation.

The code calls both updateFeature(result.feature.id, result.feature) (local Zustand state) and queryClient.invalidateQueries(). When the query refetches, it may overwrite the optimistically updated local state, causing a brief flicker or redundant re-render.

This is acceptable during migration, but long-term you may want to either:

  1. Remove the local state update and rely solely on React Query's cache, or
  2. Use queryClient.setQueryData() for optimistic updates instead of invalidation
apps/ui/src/components/codex-usage-popover.tsx (1)

83-94: Error classification fallback may be inaccurate.

The error mapping defaults to AUTH_ERROR for any message that doesn't match the "not available" or "bridge/API" patterns. This could incorrectly classify network errors, timeouts, or other non-auth-related failures as authentication errors, potentially showing misleading user guidance.

💡 Consider using UNKNOWN as the default fallback
     if (message.includes('bridge') || message.includes('API')) {
       return { code: ERROR_CODES.API_BRIDGE_UNAVAILABLE, message };
     }
-    return { code: ERROR_CODES.AUTH_ERROR, message };
+    if (message.includes('auth') || message.includes('login') || message.includes('credential')) {
+      return { code: ERROR_CODES.AUTH_ERROR, message };
+    }
+    return { code: ERROR_CODES.UNKNOWN, message };
   }, [queryError]);

You'd also need to add a UI case for UNKNOWN errors with a generic message.

apps/ui/src/components/views/settings-view/api-keys/claude-usage-section.tsx (1)

41-75: Consider aligning UsageCard with other implementations.

This UsageCard component is simpler than the versions in usage-popover.tsx and claude-usage-popover.tsx (missing isPrimary and stale props). This may be intentional for the settings view, but consider extracting a shared component to reduce duplication across the three files.

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

28-29: Unused constant: REFRESH_INTERVAL_SECONDS appears to be dead code.

This constant is defined but not used anywhere in the file. It was likely part of the previous manual polling implementation that has been replaced by React Query's built-in polling.

🧹 Proposed cleanup
-// Fixed refresh interval (45 seconds)
-const REFRESH_INTERVAL_SECONDS = 45;
-
 // Helper to format reset time for Codex

160-226: Consider extracting UsageCard to a shared component.

This UsageCard implementation (with isPrimary and stale props) is duplicated in claude-usage-popover.tsx. Consider extracting it to a shared component to reduce code duplication.

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

47-123: Duplicated components: ProgressBar and UsageCard.

These components are nearly identical to the versions in usage-popover.tsx. Consider extracting them to a shared location (e.g., @/components/ui/usage-card.tsx) to reduce duplication and ensure consistent styling across the codebase.


182-194: Consider adding structured error handling for better user guidance.

Unlike usage-popover.tsx which parses errors into specific codes (TRUST_PROMPT, API_BRIDGE_UNAVAILABLE, etc.) and provides tailored messages, this component shows a generic message for all errors. Consider aligning the error handling for consistency and better UX.

For example, if the error contains "Trust prompt" or "folder permission", guide the user to approve access in the terminal rather than suggesting claude login.

apps/ui/src/components/session-manager.tsx (1)

148-156: Avoid a potential double refetch in invalidateSessions.

invalidateQueries already refetches active queries in React Query v5; calling refetchSessions() immediately afterward can trigger a second request. Consider sticking to a single refresh path (either rely on invalidation + cache read, or just refetch once) to avoid redundant network calls.

apps/ui/src/components/views/settings-view/providers/opencode-settings-tab.tsx (2)

128-128: Consider limiting loading UI to initial fetch.
Using isFetching alone will flag loading during background refreshes; if you only want initial-load indicators, gate on empty data or isLoading.

♻️ Possible refinement
-  const isLoadingDynamicModels = isFetchingProviders || isFetchingModels;
+  const isLoadingDynamicModels =
+    (isFetchingProviders && providersData.length === 0) ||
+    (isFetchingModels && modelsData.length === 0);

135-153: Avoid repeated provider casts.
Consider hoisting the as OpenCodeProviderInfo[] cast to a single providers const (or type the hook result) to reduce duplication and improve readability.

Comment on lines 20 to +32
// Check if CLI is verified/authenticated
const isCliVerified =
claudeAuthStatus?.authenticated && claudeAuthStatus?.method === 'cli_authenticated';

// Check if data is stale (older than 2 minutes) - recalculates when claudeUsageLastUpdated changes
const isStale = useMemo(() => {
return !claudeUsageLastUpdated || Date.now() - claudeUsageLastUpdated > 2 * 60 * 1000;
}, [claudeUsageLastUpdated]);

const fetchUsage = useCallback(
async (isAutoRefresh = false) => {
if (!isAutoRefresh) setLoading(true);
setError(null);
try {
const api = getElectronAPI();
if (!api.claude) {
setError({
code: ERROR_CODES.API_BRIDGE_UNAVAILABLE,
message: 'Claude API bridge not available',
});
return;
}
const data = await api.claude.getUsage();
if ('error' in data) {
// Detect trust prompt error
const isTrustPrompt =
data.error === 'Trust prompt pending' ||
(data.message && data.message.includes('folder permission'));
setError({
code: isTrustPrompt ? ERROR_CODES.TRUST_PROMPT : ERROR_CODES.AUTH_ERROR,
message: data.message || data.error,
});
return;
}
setClaudeUsage(data);
} catch (err) {
setError({
code: ERROR_CODES.UNKNOWN,
message: err instanceof Error ? err.message : 'Failed to fetch usage',
});
} finally {
if (!isAutoRefresh) setLoading(false);
}
},
[setClaudeUsage]
);

// Auto-fetch on mount if data is stale (only if CLI is verified)
useEffect(() => {
if (isStale && isCliVerified) {
fetchUsage(true);
}
}, [isStale, isCliVerified, fetchUsage]);

useEffect(() => {
// Skip if CLI is not verified
if (!isCliVerified) return;

// Initial fetch when opened
if (open) {
if (!claudeUsage || isStale) {
fetchUsage();
}
}
// Use React Query for usage data
const {
data: claudeUsage,
isLoading,
isFetching,
error,
dataUpdatedAt,
refetch,
} = useClaudeUsage(isCliVerified);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the authentication check patterns across Claude usage components
echo "=== claude-usage-section.tsx auth check ==="
rg -n "canFetchUsage|authenticated" apps/ui/src/components/views/settings-view/api-keys/claude-usage-section.tsx

echo ""
echo "=== claude-usage-popover.tsx auth check ==="
rg -n "isCliVerified|authenticated" apps/ui/src/components/claude-usage-popover.tsx

echo ""
echo "=== usage-popover.tsx auth check ==="
rg -n "isClaudeAuthenticated|authenticated" apps/ui/src/components/usage-popover.tsx

Repository: AutoMaker-Org/automaker

Length of output: 1183


Inconsistent authentication check across Claude usage components.

claude-usage-popover.tsx checks both authenticated and method === 'cli_authenticated' (lines 21-22), while claude-usage-section.tsx (line 80) and usage-popover.tsx (line 70) only check claudeAuthStatus?.authenticated. Verify if this stricter check in the popover is intentional; if not, align the authentication logic across all three components.

🤖 Prompt for AI Agents
In `@apps/ui/src/components/claude-usage-popover.tsx` around lines 20 - 32, The
popover's authentication check (variable isCliVerified in
claude-usage-popover.tsx) is stricter than the other components—it's checking
claudeAuthStatus?.authenticated && claudeAuthStatus?.method ===
'cli_authenticated' before calling useClaudeUsage; to align behavior, remove the
method check and set isCliVerified to claudeAuthStatus?.authenticated (or, if
the stricter policy is desired, update claude-usage-section.tsx and
usage-popover.tsx to include the method check); update the isCliVerified
definition and any callers of useClaudeUsage accordingly so all three components
use the same authentication logic.

}

if (loadingPromptId || generatingPromptIds.has(prompt.id)) return;
if (loadingPromptId || generateMutation.isPending || generatingPromptIds.has(prompt.id)) return;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent disabled state: generateMutation.isPending not reflected in isDisabled.

The guard at line 63 correctly includes generateMutation.isPending, but the isDisabled variable at line 132 doesn't account for it. This causes cards to appear clickable (hover effects, pointer cursor) while the mutation is pending, even though clicks are blocked.

Proposed fix
-              const isDisabled = loadingPromptId !== null || isGenerating;
+              const isDisabled = loadingPromptId !== null || isGenerating || generateMutation.isPending;

Also applies to: 132-132

🤖 Prompt for AI Agents
In `@apps/ui/src/components/views/ideation-view/components/prompt-list.tsx` at
line 63, The disabled state is inconsistent: the click-guard checks
generateMutation.isPending but the computed isDisabled (used for
styling/hover/cursor) does not; update the isDisabled calculation in the
prompt-list component to include generateMutation.isPending (alongside
loadingPromptId and generatingPromptIds.has(prompt.id)) so that when
generateMutation.isPending is true the card shows disabled styles and pointer
events are prevented for the prompt with prompt.id.

Comment on lines +65 to +74
// Refresh all opencode-related queries
const handleRefreshOpencodeCli = useCallback(async () => {
setIsCheckingOpencodeCli(true);
setIsLoadingDynamicModels(true);
try {
const api = getElectronAPI();
if (api?.setup?.getOpencodeStatus) {
const result = await api.setup.getOpencodeStatus();
setCliStatus({
success: result.success,
status: result.installed ? 'installed' : 'not_installed',
method: result.auth?.method,
version: result.version,
path: result.path,
recommendation: result.recommendation,
installCommands: result.installCommands,
});
if (result.auth) {
setAuthStatus({
authenticated: result.auth.authenticated,
method: (result.auth.method as OpencodeAuthStatus['method']) || 'none',
hasApiKey: result.auth.hasApiKey,
hasEnvApiKey: result.auth.hasEnvApiKey,
hasOAuthToken: result.auth.hasOAuthToken,
});
}

if (result.installed) {
// Refresh providers
if (api?.setup?.getOpencodeProviders) {
const providersResult = await api.setup.getOpencodeProviders();
if (providersResult.success && providersResult.providers) {
setCachedOpencodeProviders(providersResult.providers);
}
}

// Refresh dynamic models
if (api?.setup?.refreshOpencodeModels) {
const modelsResult = await api.setup.refreshOpencodeModels();
if (modelsResult.success && modelsResult.models) {
setDynamicOpencodeModels(modelsResult.models);
}
}

toast.success('OpenCode CLI refreshed');
}
}
} catch (error) {
logger.error('Failed to refresh OpenCode CLI status:', error);
toast.error('Failed to refresh OpenCode CLI status');
} finally {
setIsCheckingOpencodeCli(false);
setIsLoadingDynamicModels(false);
}
}, [setDynamicOpencodeModels, setCachedOpencodeProviders]);
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]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n apps/ui/src/components/views/settings-view/providers/opencode-settings-tab.tsx | head -100

Repository: AutoMaker-Org/automaker

Length of output: 4434


🏁 Script executed:

# Search for invalidateQueries usage to understand the pattern
rg "invalidateQueries" apps/ui/src --type tsx --type ts -B 2 -A 2 | head -50

Repository: AutoMaker-Org/automaker

Length of output: 94


🏁 Script executed:

# Check if there's error handling in similar refresh/refetch operations
rg "refetch|invalidate" apps/ui/src/components --type tsx --type ts -B 3 -A 3 | head -80

Repository: AutoMaker-Org/automaker

Length of output: 94


🏁 Script executed:

# Search for invalidateQueries usage with proper syntax
rg "invalidateQueries" apps/ui/src -A 2 -B 2 | head -60

Repository: AutoMaker-Org/automaker

Length of output: 4979


🏁 Script executed:

# Look for error handling in async operations in the same component directory
rg "try.*catch|catch.*error" apps/ui/src/components --type-list | head -30

Repository: AutoMaker-Org/automaker

Length of output: 456


🏁 Script executed:

# Check React Query documentation pattern in the codebase
rg "Promise\.all.*invalidate" apps/ui/src -B 3 -A 5 | head -80

Repository: AutoMaker-Org/automaker

Length of output: 49


🏁 Script executed:

# Search for refetch error handling patterns and refetchCliStatus usage
rg "refetchCliStatus" apps/ui/src -B 2 -A 5

Repository: AutoMaker-Org/automaker

Length of output: 2095


🏁 Script executed:

# Look for useOpencodeCliStatus hook definition
fd "hooks" -t d apps/ui/src | head -5

Repository: AutoMaker-Org/automaker

Length of output: 322


🏁 Script executed:

# Check the query hooks file
find apps/ui/src -name "*queries*" -type f

Repository: AutoMaker-Org/automaker

Length of output: 49


🏁 Script executed:

# Find the queries hook file
find apps/ui/src/hooks -name "*queries*" -o -name "*opencode*" 2>/dev/null

Repository: AutoMaker-Org/automaker

Length of output: 92


🏁 Script executed:

# Look for hooks/queries directory
ls -la apps/ui/src/hooks/ 2>/dev/null | grep -E "queries|opencode"

Repository: AutoMaker-Org/automaker

Length of output: 125


🏁 Script executed:

# List files in queries directory
ls -la apps/ui/src/hooks/queries/

Repository: AutoMaker-Org/automaker

Length of output: 1215


🏁 Script executed:

# Search for useOpencodeCliStatus definition
rg "useOpencodeCliStatus" apps/ui/src/hooks/queries -A 10

Repository: AutoMaker-Org/automaker

Length of output: 1593


Add error handling to the refresh flow.

The refetchCliStatus() call can reject if the underlying query fails. Currently, the success toast fires unconditionally even if invalidation or refetch fails. Add a try/catch block to handle errors, matching the error handling patterns already used elsewhere in this component.

🛠️ Suggested fix
-  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]);
+  const handleRefreshOpencodeCli = useCallback(async () => {
+    try {
+      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');
+    } catch {
+      toast.error('Failed to refresh OpenCode CLI');
+    }
+  }, [queryClient, refetchCliStatus]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Refresh all opencode-related queries
const handleRefreshOpencodeCli = useCallback(async () => {
setIsCheckingOpencodeCli(true);
setIsLoadingDynamicModels(true);
try {
const api = getElectronAPI();
if (api?.setup?.getOpencodeStatus) {
const result = await api.setup.getOpencodeStatus();
setCliStatus({
success: result.success,
status: result.installed ? 'installed' : 'not_installed',
method: result.auth?.method,
version: result.version,
path: result.path,
recommendation: result.recommendation,
installCommands: result.installCommands,
});
if (result.auth) {
setAuthStatus({
authenticated: result.auth.authenticated,
method: (result.auth.method as OpencodeAuthStatus['method']) || 'none',
hasApiKey: result.auth.hasApiKey,
hasEnvApiKey: result.auth.hasEnvApiKey,
hasOAuthToken: result.auth.hasOAuthToken,
});
}
if (result.installed) {
// Refresh providers
if (api?.setup?.getOpencodeProviders) {
const providersResult = await api.setup.getOpencodeProviders();
if (providersResult.success && providersResult.providers) {
setCachedOpencodeProviders(providersResult.providers);
}
}
// Refresh dynamic models
if (api?.setup?.refreshOpencodeModels) {
const modelsResult = await api.setup.refreshOpencodeModels();
if (modelsResult.success && modelsResult.models) {
setDynamicOpencodeModels(modelsResult.models);
}
}
toast.success('OpenCode CLI refreshed');
}
}
} catch (error) {
logger.error('Failed to refresh OpenCode CLI status:', error);
toast.error('Failed to refresh OpenCode CLI status');
} finally {
setIsCheckingOpencodeCli(false);
setIsLoadingDynamicModels(false);
}
}, [setDynamicOpencodeModels, setCachedOpencodeProviders]);
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]);
// Refresh all opencode-related queries
const handleRefreshOpencodeCli = useCallback(async () => {
try {
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');
} catch {
toast.error('Failed to refresh OpenCode CLI');
}
}, [queryClient, refetchCliStatus]);
🤖 Prompt for AI Agents
In
`@apps/ui/src/components/views/settings-view/providers/opencode-settings-tab.tsx`
around lines 65 - 74, The refresh flow in handleRefreshOpencodeCli needs error
handling: wrap the invalidateQueries + refetchCliStatus calls in a try/catch,
await the Promise.all and then await refetchCliStatus inside the try block, call
toast.success('OpenCode CLI refreshed') only on success, and in the catch block
call toast.error(...) with the caught error message (or formatted error) so
failures to invalidate or refetch show an error toast; reference the
handleRefreshOpencodeCli function, queryClient.invalidateQueries,
refetchCliStatus, toast.success and toast.error when making the change.

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: 4

🤖 Fix all issues with AI agents
In `@apps/ui/src/components/views/chat-history.tsx`:
- Around line 272-275: The absolutely positioned div with className "absolute
left-0 right-0" that uses style={{ transform: `translateY(${offsetTop}px)` }}
should include an explicit top anchor to ensure consistent vertical positioning
across browsers; update that element (the div in the ChatHistory component
rendering the translated items) to add the "top-0" class to its className so it
starts at the top of its parent before the translateY is applied.

In `@apps/ui/src/components/views/graph-view/hooks/use-graph-nodes.ts`:
- Around line 86-88: The memoized nodes/edges in the useGraphNodes hook are
missing renderMode and enableEdgeAnimations in the useMemo dependency array, so
update the useMemo call inside useGraphNodes to include renderMode and
enableEdgeAnimations alongside the existing dependencies; this ensures changes
to renderMode (used in node/edge data) and enableEdgeAnimations (used for edge
animation) invalidate the memo and recompute nodes/edges correctly.

In
`@apps/ui/src/components/views/settings-view/providers/opencode-settings-tab.tsx`:
- Around line 55-63: The authStatus useMemo in opencode-settings-tab.tsx is not
mapping the optional error field from cliStatusData.auth into the
OpencodeAuthStatus object, which loses UI error details; update the return
object inside the authStatus useMemo (the function that builds
OpencodeAuthStatus) to include error: cliStatusData.auth.error (with the
appropriate cast to OpencodeAuthStatus['error'] or nullable type) so the
optional error is preserved and passed through to any components consuming
authStatus.

In `@apps/ui/src/styles/global.css`:
- Around line 1124-1127: The .perf-contain rule using content-visibility: auto
should include a contain-intrinsic-size to prevent layout shifts; update the
.perf-contain CSS (the rule that contains contain: layout paint and
content-visibility: auto) to add a contain-intrinsic-size with an appropriate
placeholder height (or use the "auto <length>" pattern, e.g., auto
<expected-height>) to reserve space until the element is painted, or if heights
vary significantly implement a dynamic approach using IntersectionObserver +
ResizeObserver to set contain-intrinsic-size at runtime.
♻️ Duplicate comments (5)
apps/ui/src/components/views/board-view.tsx (1)

1434-1439: Dual source of truth acknowledged but should be addressed.

The cache invalidation is correctly implemented, but updating both React Query cache and Zustand store creates a dual source of truth. This was previously flagged in review comments.

Consider tracking this as technical debt to remove the Zustand state for pipeline config once all consumers migrate to React Query.

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

4-8: Import paths should use @automaker/* namespace.

Per coding guidelines, imports should use @automaker/* packages instead of relative @/ paths. This applies to @/lib/utils and @/store/app-store. Based on learnings, @automaker/utils exports utilities like cn.

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

3-4: Import path should use @automaker/utils.

Per coding guidelines, replace '@/lib/utils' with '@automaker/utils' for the cn import.

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

7-12: Import paths should use @automaker/* namespace.

Per coding guidelines, replace '@/store/app-store' and '@/lib/utils' with corresponding @automaker/* packages.

apps/ui/src/components/views/settings-view/providers/opencode-settings-tab.tsx (1)

66-75: Add error handling around React Query invalidation/refetch.

The refresh flow still unconditionally shows success; failures during invalidation/refetch should surface an error toast. This mirrors a concern already raised in earlier review comments for the same lines.

🛠️ Suggested fix
   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');
+    try {
+      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');
+    } catch {
+      toast.error('Failed to refresh OpenCode CLI');
+    }
   }, [queryClient, refetchCliStatus]);
🧹 Nitpick comments (7)
apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx (1)

1-1: Consider removing @ts-nocheck directive.

This file-level TypeScript suppression disables all type checking, which can mask legitimate type errors introduced by future changes. If there are specific type issues, consider using targeted @ts-expect-error comments with explanations instead.

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

20-24: New virtualization-support props are well-designed.

The props provide a clean API for the parent VirtualizedList to control scrolling and spacing. Consider adding JSDoc comments for contentRef, onScroll, and contentStyle to match the documentation style used for width.

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

190-204: Potential render cascade from measurement updates.

Each call to registerItem that detects a size change triggers setMeasureVersion, causing the entire VirtualizedList to re-render. With many items mounting simultaneously, this could cause multiple sequential renders.

Consider batching measurement updates using a microtask or debounce:

♻️ Suggested batching approach
+ const pendingUpdatesRef = useRef(false);
+
  const registerItem = useCallback(
    (id: string) => (node: HTMLDivElement | null) => {
      if (!node || !shouldVirtualize) return;
      const measuredHeight = node.getBoundingClientRect().height;
      const previousHeight = measurementsRef.current.get(id);
      if (
        previousHeight === undefined ||
        Math.abs(previousHeight - measuredHeight) > VIRTUALIZATION_MEASURE_EPSILON_PX
      ) {
        measurementsRef.current.set(id, measuredHeight);
-       setMeasureVersion((value) => value + 1);
+       if (!pendingUpdatesRef.current) {
+         pendingUpdatesRef.current = true;
+         queueMicrotask(() => {
+           pendingUpdatesRef.current = false;
+           setMeasureVersion((value) => value + 1);
+         });
+       }
      }
    },
    [shouldVirtualize]
  );

500-629: Significant code duplication between rendering paths.

The virtualized (lines 536-584) and non-virtualized (lines 588-625) rendering paths duplicate most of the KanbanCard prop wiring. Consider extracting a shared render function to reduce maintenance burden.

♻️ Extract shared card renderer
const renderCard = (feature: Feature, index: number, isVirtualized: boolean) => {
  let shortcutKey: string | undefined;
  if (column.id === 'in_progress' && index < 10) {
    shortcutKey = index === 9 ? '0' : String(index + 1);
  }
  return (
    <KanbanCard
      key={feature.id}
      feature={feature}
      onEdit={() => onEdit(feature)}
      onDelete={() => onDelete(feature.id)}
      // ... other props
      reduceEffects={reduceEffects}
    />
  );
};

Then use it in both paths with appropriate wrappers.

apps/ui/src/components/views/chat-history.tsx (1)

159-161: Add proper type annotation for the session parameter.

Using any bypasses type checking. Consider using the appropriate session type from your store or defining an interface.

Suggested fix
-  const handleSelectSession = (session: any) => {
+  const handleSelectSession = (session: ChatSession) => {
     setCurrentChatSession(session);
   };

You'll need to import or define the ChatSession type that matches your store's session structure.

apps/ui/src/components/views/graph-view/components/dependency-edge.tsx (1)

66-135: Consider extracting the delete-button UI to avoid duplication.

Compact and full render paths now duplicate the same button markup; a small shared helper keeps behavior/styling in sync.

apps/ui/src/components/views/graph-view/components/task-node.tsx (1)

143-147: Tailwind v4 important modifier should be postfix (bg-border!).
The new compact handle classes still use the old ! prefix form.

♻️ Suggested update
-            'w-3 h-3 !bg-border border-2 border-background',
+            'w-3 h-3 bg-border! border-2 border-background',
             'transition-colors duration-200',
-            'hover:!bg-brand-500',
+            'hover:bg-brand-500!',
-            'w-3 h-3 !bg-border border-2 border-background',
+            'w-3 h-3 bg-border! border-2 border-background',
             'transition-colors duration-200',
-            'hover:!bg-brand-500',
+            'hover:bg-brand-500!',
Based on learnings, Tailwind v4 uses postfix `!` for important utilities.

Also applies to: 215-218

Comment on lines +272 to +275
<div
className="absolute left-0 right-0"
style={{ transform: `translateY(${offsetTop}px)` }}
>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add top-0 to the absolutely positioned container for predictable positioning.

Without an explicit top value, the absolutely positioned element's vertical position may behave inconsistently across browsers. Since you're using transform: translateY to position items, the container should start at the top of its parent.

Suggested fix
                <div
-                 className="absolute left-0 right-0"
+                 className="absolute top-0 left-0 right-0"
                  style={{ transform: `translateY(${offsetTop}px)` }}
                >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
className="absolute left-0 right-0"
style={{ transform: `translateY(${offsetTop}px)` }}
>
<div
className="absolute top-0 left-0 right-0"
style={{ transform: `translateY(${offsetTop}px)` }}
>
🤖 Prompt for AI Agents
In `@apps/ui/src/components/views/chat-history.tsx` around lines 272 - 275, The
absolutely positioned div with className "absolute left-0 right-0" that uses
style={{ transform: `translateY(${offsetTop}px)` }} should include an explicit
top anchor to ensure consistent vertical positioning across browsers; update
that element (the div in the ChatHistory component rendering the translated
items) to add the "top-0" class to its className so it starts at the top of its
parent before the translateY is applied.

Comment on lines +86 to 88
renderMode = GRAPH_RENDER_MODE_FULL,
enableEdgeAnimations = true,
}: UseGraphNodesProps) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd apps/ui/src/components/views/graph-view/hooks && wc -l use-graph-nodes.ts

Repository: AutoMaker-Org/automaker

Length of output: 89


🏁 Script executed:

cd apps/ui/src/components/views/graph-view/hooks && sed -n '70,110p' use-graph-nodes.ts

Repository: AutoMaker-Org/automaker

Length of output: 1612


🏁 Script executed:

cd apps/ui/src/components/views/graph-view/hooks && sed -n '180,210p' use-graph-nodes.ts

Repository: AutoMaker-Org/automaker

Length of output: 479


🏁 Script executed:

cd apps/ui/src/components/views/graph-view/hooks && sed -n '80,195p' use-graph-nodes.ts

Repository: AutoMaker-Org/automaker

Length of output: 4580


Add renderMode and enableEdgeAnimations to the useMemo dependency array.

Both renderMode (used in node and edge data at lines ~119 and ~176) and enableEdgeAnimations (used in edge animation at line ~168) are consumed within the memoized callback. Without them in the dependency array, the memoized nodes/edges become stale if these props change independently of the other dependencies, causing the graph to render with outdated render mode or animation settings.

Proposed fix
-  }, [features, runningAutoTasks, filterResult, actionCallbacks, backgroundSettings]);
+  }, [
+    features,
+    runningAutoTasks,
+    filterResult,
+    actionCallbacks,
+    backgroundSettings,
+    renderMode,
+    enableEdgeAnimations,
+  ]);
🤖 Prompt for AI Agents
In `@apps/ui/src/components/views/graph-view/hooks/use-graph-nodes.ts` around
lines 86 - 88, The memoized nodes/edges in the useGraphNodes hook are missing
renderMode and enableEdgeAnimations in the useMemo dependency array, so update
the useMemo call inside useGraphNodes to include renderMode and
enableEdgeAnimations alongside the existing dependencies; this ensures changes
to renderMode (used in node/edge data) and enableEdgeAnimations (used for edge
animation) invalidate the memo and recompute nodes/edges correctly.

@DhanushSantosh
Copy link
Collaborator

@coderabbitai Review this pr now

@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

@DhanushSantosh: I'll review the changes in this PR now. This is a comprehensive React Query migration covering query/mutation hooks, WebSocket integration, and component refactoring across the UI.

✅ 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.

@DhanushSantosh DhanushSantosh self-assigned this Jan 19, 2026
@DhanushSantosh DhanushSantosh added Enhancement Improvements to existing functionality or UI. Ready-To-Merge A feature or bug has been improved/fixed and a final review is requested before merging. Performance A feature or bug is impacting performance and this needs to be looked at, and removed Do Not Merge Use this label if something should not be merged. Work-In-Progress Currently being addressed. labels Jan 19, 2026
# Conflicts:
#	apps/ui/src/components/views/board-view.tsx
#	apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx
#	apps/ui/src/components/views/board-view/hooks/use-board-features.ts
#	apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx
#	apps/ui/src/hooks/use-project-settings-loader.ts
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: 2

Caution

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

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

284-286: Fix undefined setOutput in backlog-plan handler.

setOutput is no longer defined after the streaming migration, so this will throw. Append to streamedContent instead.

🔧 Proposed fix
-      if (newContent) {
-        setOutput((prev) => `${prev}${newContent}`);
-      }
+      if (newContent) {
+        setStreamedContent((prev) => prev + newContent);
+      }
🤖 Fix all issues with AI agents
In `@apps/ui/src/components/views/running-agents-view.tsx`:
- Around line 40-55: The backlog-plan branch in handleStopAgent calls await
api.backlogPlan.stop() without any error handling, so wrap that call in a
try/catch: call await api.backlogPlan.stop() inside a try, on success call
refetch(), and in the catch call logger.error('Failed to stop backlog plan', {
featureId: agent.featureId, error }) (and optionally surface a user notification
if available); keep the existing return after refetch to mirror the current flow
and leave the regular stopFeature.mutate path unchanged.

In `@apps/ui/src/hooks/use-project-settings-loader.ts`:
- Around line 30-47: The effect currently prevents re-applying settings when
only the settings object changes because it only compares
appliedProjectRef.current to currentProject.path; change the guard to also
consider React Query's dataUpdatedAt from useProjectSettings so subsequent
refetches re-apply: store both the project path and the settings' dataUpdatedAt
(or maintain a separate appliedUpdatedAt ref) and compare both
appliedProjectRef.current and settings.dataUpdatedAt before returning; when
applying settings update appliedProjectRef (or appliedUpdatedAt) with the new
dataUpdatedAt so future updates are detected.
♻️ Duplicate comments (1)
apps/ui/src/components/views/board-view.tsx (1)

1475-1480: Potential dual source of truth persists.
This still invalidates React Query and writes to Zustand. If both must exist, ensure they’re strictly synchronized or migrate remaining consumers.

🧹 Nitpick comments (4)
apps/ui/src/components/views/running-agents-view.tsx (2)

26-26: Move logger to module scope to avoid recreation on every render.

Creating the logger inside the component causes a new instance each render. Since the context string 'RunningAgentsView' is static, move it outside the component. This also resolves the stale closure issue in handleViewLogs (line 83) where logger is used but not in the dependency array.

♻️ Suggested fix
+const logger = createLogger('RunningAgentsView');
+
 export function RunningAgentsView() {
   const [selectedAgent, setSelectedAgent] = useState<RunningAgent | null>(null);
   const { setCurrentProject, projects } = useAppStore();
   const navigate = useNavigate();
-
-  const logger = createLogger('RunningAgentsView');

Then update the dependency arrays:

     [stopFeature, refetch, logger]
+    [stopFeature, refetch]

199-207: Consider per-agent pending state for better UX (optional).

Currently stopFeature.isPending disables all Stop buttons when any agent is being stopped. This is likely intentional to prevent race conditions. If per-agent feedback is desired, you could compare stopFeature.variables?.featureId against agent.featureId to only disable the specific button being acted upon.

♻️ Optional enhancement
                   <Button
                     variant="destructive"
                     size="sm"
                     onClick={() => handleStopAgent(agent)}
-                    disabled={stopFeature.isPending}
+                    disabled={stopFeature.isPending && stopFeature.variables?.featureId === agent.featureId}
                   >
apps/ui/src/components/views/board-view/dialogs/agent-output-modal.tsx (1)

396-410: Add guard for consistency and clarity.

TaskProgressPanel safely handles empty projectPath by returning null when tasks cannot be loaded. However, add the guard for consistency with how GitDiffPanel is handled below (line 407) and to avoid unnecessary component lifecycle operations.

Suggested guard
-        {!isBacklogPlan && (
-          <TaskProgressPanel
-            featureId={featureId}
-            projectPath={resolvedProjectPath}
-            className="shrink-0 mx-3 my-2"
-          />
-        )}
+        {!isBacklogPlan && resolvedProjectPath && (
+          <TaskProgressPanel
+            featureId={featureId}
+            projectPath={resolvedProjectPath}
+            className="shrink-0 mx-3 my-2"
+          />
+        )}
apps/ui/src/components/views/board-view/hooks/use-board-features.ts (1)

24-32: Disambiguate the two loadFeatures meanings.
The local loadFeatures (refetch) and the exported loadFeatures (invalidate) do different things, which is easy to misread. Consider renaming the refetch alias to avoid accidental misuse.

♻️ Possible refactor
-  const {
-    data: features = [],
-    isLoading,
-    refetch: loadFeatures,
-  } = useFeatures(currentProject?.path);
+  const {
+    data: features = [],
+    isLoading,
+    refetch: refetchFeatures,
+  } = useFeatures(currentProject?.path);

...
-        loadFeatures();
+        refetchFeatures();
...
-        loadFeatures();
+        refetchFeatures();

Also applies to: 108-116, 186-190

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: 0

Caution

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

⚠️ Outside diff range comments (1)
apps/ui/src/components/views/board-view/hooks/use-board-persistence.ts (1)

104-125: Add removeFeature call to persist feature deletion to local store.

persistFeatureCreate (line 90) and persistFeatureUpdate (line 52) both call updateFeature to sync the local store immediately after API success. persistFeatureDelete should similarly call removeFeature for consistency, as the method exists in useAppStore and would keep the local store in sync with the API state. Without this, deleted features could remain in the store until the query cache refetch completes, risking UI inconsistency if the callback is invoked independently.

🧹 Nitpick comments (1)
apps/ui/src/components/views/board-view/hooks/use-board-persistence.ts (1)

51-65: Redundant cache update pattern: setQueryData followed by invalidateQueries causes unnecessary refetch.

Lines 53-61 update the cache with the server response, then lines 63-65 immediately invalidate the same query, triggering a refetch. Since result.feature is the authoritative data from the API, setQueryData alone is sufficient—no refetch needed.

♻️ Suggested fix: Remove the redundant invalidation
          queryClient.setQueryData<Feature[]>(
            queryKeys.features.all(currentProject.path),
            (features) => {
              if (!features) return features;
              return features.map((feature) =>
                feature.id === updatedFeature.id ? updatedFeature : feature
              );
            }
          );
-          // Invalidate React Query cache to sync UI
-          queryClient.invalidateQueries({
-            queryKey: queryKeys.features.all(currentProject.path),
-          });

@DhanushSantosh DhanushSantosh merged commit 179c5ae into v0.13.0rc Jan 20, 2026
7 checks passed
@DhanushSantosh DhanushSantosh deleted the feat/react-query branch January 20, 2026 14:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement Improvements to existing functionality or UI. Performance A feature or bug is impacting performance and this needs to be looked at, Ready-To-Merge A feature or bug has been improved/fixed and a final review is requested before merging. Testers-Requested Request for others to test an enhancement or bug fix/etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants