diff --git a/App.tsx b/App.tsx index e735e8c..b7fe4f3 100644 --- a/App.tsx +++ b/App.tsx @@ -13,7 +13,7 @@ import TaskLogPanel from './components/TaskLogPanel'; import DebugPanel from './components/DebugPanel'; import { IconContext } from './contexts/IconContext'; import CommandPalette from './components/CommandPalette'; -import StatusBar from './components/StatusBar'; +import StatusBar, { StatusBarMessage } from './components/StatusBar'; import DirtyRepoModal from './components/modals/DirtyRepoModal'; import TaskSelectionModal from './components/modals/TaskSelectionModal'; import LaunchSelectionModal from './components/modals/LaunchSelectionModal'; @@ -174,6 +174,7 @@ const App: React.FC = () => { const [isCommandPaletteOpen, setCommandPaletteOpen] = useState(false); const [isDebugPanelOpen, setIsDebugPanelOpen] = useState(false); const [isAboutModalOpen, setIsAboutModalOpen] = useState(false); + const [statusBarMessage, setStatusBarMessage] = useState(null); const [localPathStates, setLocalPathStates] = useState>({}); const [localPathRefreshing, setLocalPathRefreshing] = useState>({}); const [detectedExecutables, setDetectedExecutables] = useState>({}); @@ -1739,6 +1740,7 @@ const App: React.FC = () => { onCancel={handleCloseRepoForm} onRefreshState={refreshRepoState} setToast={setToast} + setStatusBarMessage={setStatusBarMessage} confirmAction={confirmAction} defaultCategoryId={repoFormState.defaultCategoryId} onOpenWeblink={handleOpenWeblink} @@ -1827,6 +1829,7 @@ const App: React.FC = () => { processingCount={isProcessing.size} isSimulationMode={settings.simulationMode} latestLog={latestLog} + statusMessage={statusBarMessage} appVersion={appVersion} onToggleDebugPanel={() => setIsDebugPanelOpen(p => !p)} onOpenAboutModal={() => setIsAboutModalOpen(true)} diff --git a/components/StatusBar.tsx b/components/StatusBar.tsx index 212ed7c..e04eddd 100644 --- a/components/StatusBar.tsx +++ b/components/StatusBar.tsx @@ -7,18 +7,24 @@ import { KeyboardIcon } from './icons/KeyboardIcon'; import { BugAntIcon } from './icons/BugAntIcon'; import { useTooltip } from '../hooks/useTooltip'; +export type StatusBarMessage = { + text: string; + tone?: 'default' | 'info' | 'success' | 'warning' | 'danger'; +}; + interface StatusBarProps { repoCount: number; processingCount: number; isSimulationMode: boolean; latestLog: LogEntry | null; + statusMessage?: StatusBarMessage | null; appVersion: string; onToggleDebugPanel: () => void; onOpenAboutModal: () => void; commandPaletteShortcut: string; } -const StatusBar: React.FC = ({ repoCount, processingCount, isSimulationMode, latestLog, appVersion, onToggleDebugPanel, onOpenAboutModal, commandPaletteShortcut }) => { +const StatusBar: React.FC = ({ repoCount, processingCount, isSimulationMode, latestLog, statusMessage, appVersion, onToggleDebugPanel, onOpenAboutModal, commandPaletteShortcut }) => { const LOG_LEVEL_COLOR_CLASSES: Record = { info: 'text-gray-500 dark:text-gray-400', command: 'text-blue-500 dark:text-blue-400', @@ -27,12 +33,20 @@ const StatusBar: React.FC = ({ repoCount, processingCount, isSim warn: 'text-yellow-500 dark:text-yellow-400', }; + const STATUS_MESSAGE_COLORS: Record, string> = { + default: 'text-gray-600 dark:text-gray-300', + info: 'text-blue-600 dark:text-blue-400', + success: 'text-green-600 dark:text-green-400', + warning: 'text-amber-600 dark:text-amber-400', + danger: 'text-red-600 dark:text-red-400', + }; + const repoCountTooltip = useTooltip('Total Repositories'); const processingTooltip = useTooltip(`${processingCount} tasks running`); const simModeTooltip = useTooltip('Simulation mode is active. No real commands will be run.'); const commandPaletteTooltip = useTooltip(`Command Palette (${commandPaletteShortcut || 'Ctrl+K'})`); const debugPanelTooltip = useTooltip('Toggle Debug Panel (Ctrl+D)'); - const latestLogTooltip = useTooltip(latestLog?.message || ''); + const centerTooltip = useTooltip(statusMessage?.text || latestLog?.message || ''); const aboutTooltip = useTooltip('About this application'); return ( @@ -58,12 +72,16 @@ const StatusBar: React.FC = ({ repoCount, processingCount, isSim {/* Center Section */}
- {latestLog && ( + {...centerTooltip} className="flex-1 text-center truncate px-4"> + {statusMessage ? ( + + {statusMessage.text} + + ) : latestLog ? ( [{new Date(latestLog.timestamp).toLocaleTimeString()}] {latestLog.message} - )} + ) : null}
{/* Right Section */} diff --git a/components/modals/RepoFormModal.tsx b/components/modals/RepoFormModal.tsx index 4768baa..7cba989 100644 --- a/components/modals/RepoFormModal.tsx +++ b/components/modals/RepoFormModal.tsx @@ -38,6 +38,7 @@ import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { PencilIcon } from '../icons/PencilIcon'; import { ArrowPathIcon } from '../icons/ArrowPathIcon'; +import type { StatusBarMessage } from '../StatusBar'; interface RepoEditViewProps { onSave: (repository: Repository, categoryId?: string) => void; @@ -45,6 +46,7 @@ interface RepoEditViewProps { repository: Repository | null; onRefreshState: (repoId: string) => Promise; setToast: (toast: { message: string; type: 'success' | 'error' | 'info' } | null) => void; + setStatusBarMessage?: (message: StatusBarMessage | null) => void; confirmAction: (options: { title: string; message: React.ReactNode; @@ -1869,7 +1871,7 @@ const CommitListItem: React.FC = ({ commit, highlight }) => ); }; -const RepoEditView: React.FC = ({ onSave, onCancel, repository, onRefreshState, setToast, confirmAction, defaultCategoryId, onOpenWeblink, detectedExecutables }) => { +const RepoEditView: React.FC = ({ onSave, onCancel, repository, onRefreshState, setToast, setStatusBarMessage, confirmAction, defaultCategoryId, onOpenWeblink, detectedExecutables }) => { const logger = useLogger(); const [formData, setFormData] = useState>(() => repository || NEW_REPO_TEMPLATE); @@ -1967,6 +1969,43 @@ const RepoEditView: React.FC = ({ onSave, onCancel, repositor return keySet; }, [selectedBranches]); + const branchSelectionStats = useMemo(() => { + const selectedBranchCount = selectedBranches.length; + let selectedLocalCount = 0; + selectedBranches.forEach(selection => { + if (selection.scope === 'local') { + selectedLocalCount += 1; + } + }); + const selectedRemoteCount = selectedBranchCount - selectedLocalCount; + const isCurrentSelection = Boolean( + selectedBranchCount === 1 && + primarySelectedBranch?.scope === 'local' && + branchInfo?.current && + primarySelectedBranch.name === branchInfo.current + ); + + let selectionDescription: string | null = null; + if (!selectedBranchCount) { + selectionDescription = 'Select a branch to checkout.'; + } else if (selectedBranchCount === 1 && primarySelectedBranch) { + if (!(primarySelectedBranch.scope === 'local' && branchInfo?.current === primarySelectedBranch.name)) { + selectionDescription = `${primarySelectedBranch.scope === 'remote' ? 'Remote' : 'Local'} branch: ${primarySelectedBranch.name}`; + } + } else { + const parts: string[] = []; + if (selectedLocalCount) { + parts.push(`${selectedLocalCount} local`); + } + if (selectedRemoteCount) { + parts.push(`${selectedRemoteCount} remote`); + } + selectionDescription = `${selectedBranchCount} branches selected (${parts.join(', ')})`; + } + + return { selectedBranchCount, selectedLocalCount, selectedRemoteCount, isCurrentSelection, selectionDescription }; + }, [selectedBranches, primarySelectedBranch, branchInfo?.current]); + // State for Releases Tab const [releases, setReleases] = useState(null); const [releasesLoading, setReleasesLoading] = useState(false); @@ -2035,6 +2074,23 @@ const RepoEditView: React.FC = ({ onSave, onCancel, repositor }); }, [filteredLocalBranches, filteredRemoteBranches, branchInfo?.current]); + useEffect(() => { + if (!setStatusBarMessage) { + return; + } + if (activeTab === 'branches' && branchSelectionStats.selectionDescription) { + setStatusBarMessage({ text: branchSelectionStats.selectionDescription, tone: 'info' }); + } else { + setStatusBarMessage(null); + } + }, [activeTab, branchSelectionStats.selectionDescription, setStatusBarMessage]); + + useEffect(() => { + return () => { + setStatusBarMessage?.(null); + }; + }, [setStatusBarMessage]); + useEffect(() => { if (!branchInfo) { return; @@ -3500,32 +3556,13 @@ const RepoEditView: React.FC = ({ onSave, onCancel, repositor if (!supportsBranchTab) { return
Branch management is only available for Git or SVN repositories.
; } - const selectedBranchCount = selectedBranches.length; - const selectedLocalCount = selectedBranches.filter(selection => selection.scope === 'local').length; - const selectedRemoteCount = selectedBranchCount - selectedLocalCount; - const isCurrentSelection = Boolean( - selectedBranchCount === 1 && - primarySelectedBranch?.scope === 'local' && - branchInfo?.current && - primarySelectedBranch.name === branchInfo.current - ); + const { + selectedBranchCount, + selectedLocalCount, + selectedRemoteCount, + isCurrentSelection, + } = branchSelectionStats; const checkoutDisabled = selectedBranchCount !== 1 || isCheckoutLoading || branchesLoading || isCurrentSelection; - const selectionDescription = (() => { - if (!selectedBranchCount) { - return 'Select a branch to checkout.'; - } - if (selectedBranchCount === 1 && primarySelectedBranch) { - return `${primarySelectedBranch.scope === 'remote' ? 'Remote' : 'Local'} branch: ${primarySelectedBranch.name}`; - } - const parts: string[] = []; - if (selectedLocalCount) { - parts.push(`${selectedLocalCount} local`); - } - if (selectedRemoteCount) { - parts.push(`${selectedRemoteCount} remote`); - } - return `${selectedBranchCount} branches selected (${parts.join(', ')})`; - })(); const hasProtectedSelection = selectedBranches.some(selection => isProtectedBranch(selection.name, selection.scope)); const hasCurrentBranchSelected = selectedBranches.some(selection => selection.scope === 'local' && branchInfo?.current && selection.name === branchInfo.current); const hasDeletableSelection = selectedBranches.some(selection => { @@ -3562,15 +3599,17 @@ const RepoEditView: React.FC = ({ onSave, onCancel, repositor

Current branch: {branchInfo?.current}

-
- setBranchFilter(event.target.value)} - placeholder="Filter branches" - className={`${formInputStyle} flex-1 min-w-[12rem]`} - /> -
+
+
+ setBranchFilter(event.target.value)} + placeholder="Filter branches" + className={`${formInputStyle} w-full min-w-[12rem]`} + /> +
+
)} + {isGitRepo && ( + + )} +

Tip: Use Shift or Ctrl/Cmd-click to select multiple branches.

@@ -3696,30 +3754,6 @@ const RepoEditView: React.FC = ({ onSave, onCancel, repositor
-
-

{selectionDescription}

-
- {isGitRepo && ( - - )} - -
-
{isSvnRepo && (

Branch creation, deletion, and merging are currently only available for Git repositories.