auto-claude: subtask-9-3 - Add loading states to pop-out buttons during window creation

- Added Loader2 spinner icon to pop-out buttons while windows are being created
- Buttons are now disabled during window creation to prevent duplicate requests
- Updated SortableProjectTab to accept isLoading prop and show spinner
- ProjectTabBar now passes loading state from window-store to tabs
- Sidebar pop-out buttons show spinner and are disabled while loading
- Added i18n keys for "Creating window..." in both English and French
- Loading state tracked via window-store's loadingWindows Set

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
This commit is contained in:
AndyMik90
2026-02-15 17:20:21 +01:00
co-authored by Claude Sonnet 4.5
parent 5542b7db71
commit 6d3ab81136
7 changed files with 37 additions and 18 deletions
@@ -30,7 +30,7 @@ export function ProjectTabBar({
onSettingsClick
}: ProjectTabBarProps) {
const { t } = useTranslation('common');
const { isProjectPoppedOut, setWindowLoading, addPoppedOutProject } = useWindowStore();
const { isProjectPoppedOut, setWindowLoading, addPoppedOutProject, isWindowLoading } = useWindowStore();
// Handler for popping out a project into a new window
const handlePopOutProject = async (projectId: string) => {
@@ -148,6 +148,7 @@ export function ProjectTabBar({
onSettingsClick={isActiveTab ? onSettingsClick : undefined}
onPopOutClick={isActiveTab ? () => handlePopOutProject(project.id) : undefined}
isPoppedOut={isProjectPoppedOut(project.id)}
isLoading={isWindowLoading(project.id)}
/>
);
})}
@@ -23,7 +23,8 @@ import {
Wrench,
PanelLeft,
PanelLeftClose,
ExternalLink
ExternalLink,
Loader2
} from 'lucide-react';
import { Button } from './ui/button';
import { ScrollArea } from './ui/scroll-area';
@@ -113,7 +114,7 @@ export function Sidebar({
const projects = useProjectStore((state) => state.projects);
const selectedProjectId = useProjectStore((state) => state.selectedProjectId);
const settings = useSettingsStore((state) => state.settings);
const { isViewPoppedOut, setWindowLoading, addPoppedOutView } = useWindowStore();
const { isViewPoppedOut, setWindowLoading, addPoppedOutView, isWindowLoading, getViewKey } = useWindowStore();
const [showAddProjectModal, setShowAddProjectModal] = useState(false);
const [showInitDialog, setShowInitDialog] = useState(false);
@@ -338,6 +339,8 @@ export function Sidebar({
const isActive = activeView === item.id;
const Icon = item.icon;
const isPoppedOut = selectedProjectId ? isViewPoppedOut(selectedProjectId, item.id) : false;
const viewKey = selectedProjectId ? getViewKey(selectedProjectId, item.id) : '';
const isLoading = viewKey ? isWindowLoading(viewKey) : false;
const button = (
<div
@@ -379,26 +382,31 @@ export function Sidebar({
type="button"
onClick={(e) => {
e.stopPropagation();
if (!isPoppedOut) {
if (!isPoppedOut && !isLoading) {
handlePopOutView(item.id, e);
}
}}
disabled={isPoppedOut}
disabled={isPoppedOut || isLoading}
aria-label={isPoppedOut ? t('navigation:viewPoppedOut') : t('navigation:popOutView')}
className={cn(
'h-5 w-5 p-0 rounded flex items-center justify-center',
'text-muted-foreground hover:text-foreground',
'hover:bg-muted/50 transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',
isPoppedOut ? 'opacity-100 text-primary' : 'opacity-0 group-hover:opacity-100',
isPoppedOut && 'cursor-not-allowed'
isPoppedOut || isLoading ? 'opacity-100' : 'opacity-0 group-hover:opacity-100',
isPoppedOut && 'text-primary',
(isPoppedOut || isLoading) && 'cursor-not-allowed'
)}
>
<ExternalLink className="h-3 w-3" />
{isLoading ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<ExternalLink className="h-3 w-3" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="right">
<span>{isPoppedOut ? t('navigation:tooltips.viewPoppedOut') : t('navigation:tooltips.popOutView')}</span>
<span>{isLoading ? t('navigation:tooltips.popOutLoading') : isPoppedOut ? t('navigation:tooltips.viewPoppedOut') : t('navigation:tooltips.popOutView')}</span>
</TooltipContent>
</Tooltip>
)}
@@ -1,7 +1,7 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { useTranslation } from 'react-i18next';
import { Settings2, ExternalLink } from 'lucide-react';
import { Settings2, ExternalLink, Loader2 } from 'lucide-react';
import { cn } from '../lib/utils';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import type { Project } from '../../shared/types';
@@ -17,6 +17,7 @@ interface SortableProjectTabProps {
onSettingsClick?: () => void;
onPopOutClick?: () => void;
isPoppedOut?: boolean;
isLoading?: boolean;
}
// Detect if running on macOS for keyboard shortcut display
@@ -32,7 +33,8 @@ export function SortableProjectTab({
onClose,
onSettingsClick,
onPopOutClick,
isPoppedOut = false
isPoppedOut = false,
isLoading = false
}: SortableProjectTabProps) {
const { t } = useTranslation('common');
// Build tooltip with keyboard shortcut hint (only for tabs 1-9)
@@ -138,22 +140,26 @@ export function SortableProjectTab({
'text-muted-foreground hover:text-foreground',
'hover:bg-muted/50 transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',
isPoppedOut && 'opacity-50 cursor-not-allowed'
(isPoppedOut || isLoading) && 'opacity-50 cursor-not-allowed'
)}
onClick={(e) => {
e.stopPropagation();
if (!isPoppedOut) {
if (!isPoppedOut && !isLoading) {
onPopOutClick();
}
}}
disabled={isPoppedOut}
disabled={isPoppedOut || isLoading}
aria-label={t('projectTab.popOutAriaLabel')}
>
<ExternalLink className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
{isLoading ? (
<Loader2 className="h-3 w-3 sm:h-3.5 sm:w-3.5 animate-spin" />
) : (
<ExternalLink className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
<span>{t('projectTab.popOut')}</span>
<span>{isLoading ? t('projectTab.popOutLoading') : t('projectTab.popOut')}</span>
</TooltipContent>
</Tooltip>
)}
@@ -10,6 +10,7 @@
"addProjectAriaLabel": "Add project",
"popOut": "Pop out project",
"popOutAriaLabel": "Pop out project into separate window",
"popOutLoading": "Creating window...",
"poppedOutBadge": "Popped Out"
},
"accessibility": {
@@ -32,7 +32,8 @@
"settings": "Application Settings",
"help": "Help & Feedback",
"popOutView": "Open this view in a separate window",
"viewPoppedOut": "This view is opened in a separate window"
"viewPoppedOut": "This view is opened in a separate window",
"popOutLoading": "Creating window..."
},
"messages": {
"initializeToCreateTasks": "Initialize Auto Claude to create tasks"
@@ -10,6 +10,7 @@
"addProjectAriaLabel": "Ajouter un projet",
"popOut": "Ouvrir dans une fenêtre séparée",
"popOutAriaLabel": "Ouvrir le projet dans une fenêtre séparée",
"popOutLoading": "Création de la fenêtre...",
"poppedOutBadge": "Détaché"
},
"accessibility": {
@@ -32,7 +32,8 @@
"settings": "Paramètres de l'application",
"help": "Aide & Feedback",
"popOutView": "Ouvrir cette vue dans une fenêtre séparée",
"viewPoppedOut": "Cette vue est ouverte dans une fenêtre séparée"
"viewPoppedOut": "Cette vue est ouverte dans une fenêtre séparée",
"popOutLoading": "Création de la fenêtre..."
},
"messages": {
"initializeToCreateTasks": "Initialisez Auto Claude pour créer des tâches"