/** * WorkspaceIndicator - Floating bottom-right pill badge for workspace switching. * * Shows active workspace (Local or SSH host) with connection status badge. * Clicking opens an upward dropdown to switch between available workspaces. * Only renders when multiple contexts are available (hidden in local-only mode). */ import { useEffect, useRef, useState } from 'react'; import { useStore } from '@renderer/store'; import { Check, ChevronDown } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; import { ConnectionStatusBadge } from './ConnectionStatusBadge'; export const WorkspaceIndicator = (): React.JSX.Element | null => { const { activeContextId, isContextSwitching, availableContexts, switchContext } = useStore( useShallow((s) => ({ activeContextId: s.activeContextId, isContextSwitching: s.isContextSwitching, availableContexts: s.availableContexts, switchContext: s.switchContext, })) ); const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); // Close dropdown on outside click useEffect(() => { function handleClickOutside(event: MouseEvent): void { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsOpen(false); } } document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); // Close dropdown on Escape key useEffect(() => { function handleEscape(event: KeyboardEvent): void { if (event.key === 'Escape') { setIsOpen(false); } } document.addEventListener('keydown', handleEscape); return () => document.removeEventListener('keydown', handleEscape); }, []); // Only show when multiple contexts exist if (availableContexts.length <= 1) return null; const getContextLabel = (contextId: string): string => { if (contextId === 'local') return 'Local'; return contextId.startsWith('ssh-') ? contextId.slice(4) : contextId; }; const activeLabel = getContextLabel(activeContextId); return (
{/* Trigger pill */} {/* Upward dropdown */} {isOpen && !isContextSwitching && ( <> {/* Backdrop */}
setIsOpen(false)} /> {/* Dropdown content - opens upward */}
{/* Header */}
Switch Workspace
{/* Context list */} {availableContexts.map((ctx) => { const isSelected = ctx.id === activeContextId; const label = getContextLabel(ctx.id); return ( { void switchContext(ctx.id); setIsOpen(false); }} /> ); })}
)}
); }; /** * Individual context item in the dropdown. */ interface ContextItemProps { contextId: string; label: string; isSelected: boolean; onSelect: () => void; } const ContextItem = ({ contextId, label, isSelected, onSelect, }: Readonly): React.JSX.Element => { const [isHovered, setIsHovered] = useState(false); const buttonStyle: React.CSSProperties = isSelected ? { backgroundColor: 'var(--color-surface-raised)', color: 'var(--color-text)' } : { backgroundColor: isHovered ? 'var(--color-surface-raised)' : 'transparent', opacity: isHovered ? 0.5 : 1, }; return ( ); };