diff --git a/apps/ui/src/components/ui/button.tsx b/apps/ui/src/components/ui/button.tsx
index a7163ed3..683d1237 100644
--- a/apps/ui/src/components/ui/button.tsx
+++ b/apps/ui/src/components/ui/button.tsx
@@ -3,7 +3,7 @@ import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
-import { Spinner } from '@/components/ui/spinner';
+import { Spinner, type SpinnerVariant } from '@/components/ui/spinner';
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-200 cursor-pointer disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive active:scale-[0.98]",
@@ -37,9 +37,19 @@ const buttonVariants = cva(
}
);
-// Loading spinner component
-function ButtonSpinner({ className }: { className?: string }) {
- return ;
+/** Button variants that have colored backgrounds requiring foreground spinner color */
+const COLORED_BACKGROUND_VARIANTS = new Set(['default', 'destructive']);
+
+/** Get spinner variant based on button variant - use foreground for colored backgrounds */
+function getSpinnerVariant(
+ buttonVariant: VariantProps['variant']
+): SpinnerVariant {
+ const variant = buttonVariant ?? 'default';
+ if (COLORED_BACKGROUND_VARIANTS.has(variant)) {
+ return 'foreground';
+ }
+ // outline, secondary, ghost, link, animated-outline use standard backgrounds
+ return 'primary';
}
function Button({
@@ -57,6 +67,7 @@ function Button({
loading?: boolean;
}) {
const isDisabled = disabled || loading;
+ const spinnerVariant = getSpinnerVariant(variant);
// Special handling for animated-outline variant
if (variant === 'animated-outline' && !asChild) {
@@ -83,7 +94,7 @@ function Button({
size === 'icon' && 'p-0 gap-0'
)}
>
- {loading && }
+ {loading && }
{children}
@@ -99,7 +110,7 @@ function Button({
disabled={isDisabled}
{...props}
>
- {loading && }
+ {loading && }
{children}
);
diff --git a/apps/ui/src/components/ui/spinner.tsx b/apps/ui/src/components/ui/spinner.tsx
index c66b7684..d515dc7b 100644
--- a/apps/ui/src/components/ui/spinner.tsx
+++ b/apps/ui/src/components/ui/spinner.tsx
@@ -1,7 +1,8 @@
import { Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
-type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+export type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
+export type SpinnerVariant = 'primary' | 'foreground' | 'muted';
const sizeClasses: Record = {
xs: 'h-3 w-3',
@@ -11,9 +12,17 @@ const sizeClasses: Record = {
xl: 'h-8 w-8',
};
+const variantClasses: Record = {
+ primary: 'text-primary',
+ foreground: 'text-primary-foreground',
+ muted: 'text-muted-foreground',
+};
+
interface SpinnerProps {
/** Size of the spinner */
size?: SpinnerSize;
+ /** Color variant - use 'foreground' when on primary backgrounds */
+ variant?: SpinnerVariant;
/** Additional class names */
className?: string;
}
@@ -21,11 +30,12 @@ interface SpinnerProps {
/**
* Themed spinner component using the primary brand color.
* Use this for all loading indicators throughout the app for consistency.
+ * Use variant='foreground' when placing on primary-colored backgrounds.
*/
-export function Spinner({ size = 'md', className }: SpinnerProps) {
+export function Spinner({ size = 'md', variant = 'primary', className }: SpinnerProps) {
return (
);
diff --git a/apps/ui/src/components/ui/task-progress-panel.tsx b/apps/ui/src/components/ui/task-progress-panel.tsx
index 4fecefbc..58174d66 100644
--- a/apps/ui/src/components/ui/task-progress-panel.tsx
+++ b/apps/ui/src/components/ui/task-progress-panel.tsx
@@ -261,7 +261,7 @@ export function TaskProgressPanel({
)}
>
{isCompleted && }
- {isActive && }
+ {isActive && }
{isPending && }
diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx
index 30df9657..8a53fc6f 100644
--- a/apps/ui/src/components/views/board-view.tsx
+++ b/apps/ui/src/components/views/board-view.tsx
@@ -463,6 +463,16 @@ export function BoardView() {
const selectedWorktreeBranch =
currentWorktreeBranch || worktrees.find((w) => w.isMain)?.branch || 'main';
+ // Aggregate running auto tasks across all worktrees for this project
+ const autoModeByWorktree = useAppStore((state) => state.autoModeByWorktree);
+ const runningAutoTasksAllWorktrees = useMemo(() => {
+ if (!currentProject?.id) return [];
+ const prefix = `${currentProject.id}::`;
+ return Object.entries(autoModeByWorktree)
+ .filter(([key]) => key.startsWith(prefix))
+ .flatMap(([, state]) => state.runningTasks ?? []);
+ }, [autoModeByWorktree, currentProject?.id]);
+
// Get in-progress features for keyboard shortcuts (needed before actions hook)
// Must be after runningAutoTasks is defined
const inProgressFeaturesForShortcuts = useMemo(() => {
@@ -1372,7 +1382,7 @@ export function BoardView() {
setWorktreeRefreshKey((k) => k + 1);
}}
onRemovedWorktrees={handleRemovedWorktrees}
- runningFeatureIds={runningAutoTasks}
+ runningFeatureIds={runningAutoTasksAllWorktrees}
branchCardCounts={branchCardCounts}
features={hookFeatures.map((f) => ({
id: f.id,
diff --git a/apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx
index 7bb1440a..7ad02fa8 100644
--- a/apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx
+++ b/apps/ui/src/components/views/board-view/dialogs/merge-worktree-dialog.tsx
@@ -330,7 +330,7 @@ export function MergeWorktreeDialog({
>
{isLoading ? (
<>
-
+
Merging...
>
) : (
diff --git a/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx
index f0e64102..f0dde39d 100644
--- a/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx
+++ b/apps/ui/src/components/views/board-view/dialogs/plan-approval-dialog.tsx
@@ -210,7 +210,7 @@ export function PlanApprovalDialog({
className="bg-green-600 hover:bg-green-700 text-white"
>
{isLoading ? (
-
+
) : (
)}
diff --git a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx
index 25a79f96..a4722406 100644
--- a/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx
+++ b/apps/ui/src/components/views/board-view/worktree-panel/components/worktree-tab.tsx
@@ -260,8 +260,10 @@ export function WorktreeTab({
aria-label={worktree.branch}
data-testid={`worktree-branch-${worktree.branch}`}
>
- {isRunning && }
- {isActivating && !isRunning && }
+ {isRunning && }
+ {isActivating && !isRunning && (
+
+ )}
{worktree.branch}
{cardCount !== undefined && cardCount > 0 && (
@@ -327,8 +329,10 @@ export function WorktreeTab({
: 'Click to switch to this branch'
}
>
- {isRunning && }
- {isActivating && !isRunning && }
+ {isRunning && }
+ {isActivating && !isRunning && (
+
+ )}
{worktree.branch}
{cardCount !== undefined && cardCount > 0 && (
diff --git a/apps/ui/src/components/views/interview-view.tsx b/apps/ui/src/components/views/interview-view.tsx
index b56971c1..b30d285a 100644
--- a/apps/ui/src/components/views/interview-view.tsx
+++ b/apps/ui/src/components/views/interview-view.tsx
@@ -572,7 +572,7 @@ export function InterviewView() {
>
{isGenerating ? (
<>
-
+
Creating...
>
) : (
diff --git a/apps/ui/src/components/views/login-view.tsx b/apps/ui/src/components/views/login-view.tsx
index 0ed259bf..154df9a1 100644
--- a/apps/ui/src/components/views/login-view.tsx
+++ b/apps/ui/src/components/views/login-view.tsx
@@ -448,7 +448,7 @@ export function LoginView() {
>
{isLoggingIn ? (
<>
-
+
Authenticating...
>
) : (
diff --git a/apps/ui/src/components/views/setup-view/components/cli-installation-card.tsx b/apps/ui/src/components/views/setup-view/components/cli-installation-card.tsx
index 4932ef29..de0c3bf9 100644
--- a/apps/ui/src/components/views/setup-view/components/cli-installation-card.tsx
+++ b/apps/ui/src/components/views/setup-view/components/cli-installation-card.tsx
@@ -60,7 +60,7 @@ export function CliInstallationCard({
>
{isInstalling ? (
<>
-
+
Installing...
>
) : (
diff --git a/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx b/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx
index 87bf6f77..127b88ef 100644
--- a/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx
+++ b/apps/ui/src/components/views/setup-view/steps/claude-setup-step.tsx
@@ -412,7 +412,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps
>
{isInstalling ? (
<>
-
+
Installing...
>
) : (
@@ -574,7 +574,7 @@ export function ClaudeSetupStep({ onNext, onBack, onSkip }: ClaudeSetupStepProps
>
{isSavingApiKey ? (
<>
-
+
Saving...
>
) : (
diff --git a/apps/ui/src/components/views/setup-view/steps/cli-setup-step.tsx b/apps/ui/src/components/views/setup-view/steps/cli-setup-step.tsx
index 031d6815..4a113211 100644
--- a/apps/ui/src/components/views/setup-view/steps/cli-setup-step.tsx
+++ b/apps/ui/src/components/views/setup-view/steps/cli-setup-step.tsx
@@ -408,7 +408,7 @@ export function CliSetupStep({ config, state, onNext, onBack, onSkip }: CliSetup
>
{isInstalling ? (
<>
-
+
Installing...
>
) : (
@@ -681,7 +681,7 @@ export function CliSetupStep({ config, state, onNext, onBack, onSkip }: CliSetup
>
{isSavingApiKey ? (
<>
-
+
Saving...
>
) : (
diff --git a/apps/ui/src/components/views/setup-view/steps/cursor-setup-step.tsx b/apps/ui/src/components/views/setup-view/steps/cursor-setup-step.tsx
index e48057c4..7f03a8ee 100644
--- a/apps/ui/src/components/views/setup-view/steps/cursor-setup-step.tsx
+++ b/apps/ui/src/components/views/setup-view/steps/cursor-setup-step.tsx
@@ -318,7 +318,7 @@ export function CursorSetupStep({ onNext, onBack, onSkip }: CursorSetupStepProps
>
{isLoggingIn ? (
<>
-
+
Waiting for login...
>
) : (
diff --git a/apps/ui/src/components/views/setup-view/steps/opencode-setup-step.tsx b/apps/ui/src/components/views/setup-view/steps/opencode-setup-step.tsx
index 58337851..ac0e661a 100644
--- a/apps/ui/src/components/views/setup-view/steps/opencode-setup-step.tsx
+++ b/apps/ui/src/components/views/setup-view/steps/opencode-setup-step.tsx
@@ -316,7 +316,7 @@ export function OpencodeSetupStep({ onNext, onBack, onSkip }: OpencodeSetupStepP
>
{isLoggingIn ? (
<>
-
+
Waiting for login...
>
) : (
diff --git a/apps/ui/src/components/views/setup-view/steps/providers-setup-step.tsx b/apps/ui/src/components/views/setup-view/steps/providers-setup-step.tsx
index 40d19f8a..1a934732 100644
--- a/apps/ui/src/components/views/setup-view/steps/providers-setup-step.tsx
+++ b/apps/ui/src/components/views/setup-view/steps/providers-setup-step.tsx
@@ -329,7 +329,7 @@ function ClaudeContent() {
>
{isInstalling ? (
<>
-
+
Installing...
>
) : (
@@ -424,7 +424,11 @@ function ClaudeContent() {
disabled={isSavingApiKey || !apiKey.trim()}
className="flex-1 bg-brand-500 hover:bg-brand-600 text-white"
>
- {isSavingApiKey ? : 'Save API Key'}
+ {isSavingApiKey ? (
+
+ ) : (
+ 'Save API Key'
+ )}
{hasApiKey && (
@@ -1194,7 +1198,7 @@ function OpencodeContent() {
>
{isLoggingIn ? (
<>
-
+
Waiting for login...
>
) : (
@@ -1466,7 +1470,7 @@ function GeminiContent() {
>
{isLoggingIn ? (
<>
-
+
Waiting for login...
>
) : (
@@ -1509,7 +1513,7 @@ function GeminiContent() {
disabled={isSaving || !apiKey.trim()}
className="w-full bg-brand-500 hover:bg-brand-600 text-white"
>
- {isSaving ? : 'Save API Key'}
+ {isSaving ? : 'Save API Key'}
@@ -1745,7 +1749,7 @@ function CopilotContent() {
>
{isLoggingIn ? (
<>
-
+
Waiting for login...
>
) : (