Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<StatusBarMessage | null>(null);
const [localPathStates, setLocalPathStates] = useState<Record<string, LocalPathState>>({});
const [localPathRefreshing, setLocalPathRefreshing] = useState<Record<string, boolean>>({});
const [detectedExecutables, setDetectedExecutables] = useState<Record<string, string[]>>({});
Expand Down Expand Up @@ -1739,6 +1740,7 @@ const App: React.FC = () => {
onCancel={handleCloseRepoForm}
onRefreshState={refreshRepoState}
setToast={setToast}
setStatusBarMessage={setStatusBarMessage}
confirmAction={confirmAction}
defaultCategoryId={repoFormState.defaultCategoryId}
onOpenWeblink={handleOpenWeblink}
Expand Down Expand Up @@ -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)}
Expand Down
28 changes: 23 additions & 5 deletions components/StatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<StatusBarProps> = ({ repoCount, processingCount, isSimulationMode, latestLog, appVersion, onToggleDebugPanel, onOpenAboutModal, commandPaletteShortcut }) => {
const StatusBar: React.FC<StatusBarProps> = ({ repoCount, processingCount, isSimulationMode, latestLog, statusMessage, appVersion, onToggleDebugPanel, onOpenAboutModal, commandPaletteShortcut }) => {
const LOG_LEVEL_COLOR_CLASSES: Record<string, string> = {
info: 'text-gray-500 dark:text-gray-400',
command: 'text-blue-500 dark:text-blue-400',
Expand All @@ -27,12 +33,20 @@ const StatusBar: React.FC<StatusBarProps> = ({ repoCount, processingCount, isSim
warn: 'text-yellow-500 dark:text-yellow-400',
};

const STATUS_MESSAGE_COLORS: Record<NonNullable<StatusBarMessage['tone']>, 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 (
Expand All @@ -58,12 +72,16 @@ const StatusBar: React.FC<StatusBarProps> = ({ repoCount, processingCount, isSim

{/* Center Section */}
<div
{...latestLogTooltip} className="flex-1 text-center truncate px-4">
{latestLog && (
{...centerTooltip} className="flex-1 text-center truncate px-4">
{statusMessage ? (
<span className={STATUS_MESSAGE_COLORS[statusMessage.tone || 'default']}>
{statusMessage.text}
</span>
) : latestLog ? (
<span className={LOG_LEVEL_COLOR_CLASSES[latestLog.level] || 'text-gray-400'}>
[{new Date(latestLog.timestamp).toLocaleTimeString()}] {latestLog.message}
</span>
)}
) : null}
</div>

{/* Right Section */}
Expand Down
152 changes: 93 additions & 59 deletions components/modals/RepoFormModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ 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;
onCancel: () => void;
repository: Repository | null;
onRefreshState: (repoId: string) => Promise<void>;
setToast: (toast: { message: string; type: 'success' | 'error' | 'info' } | null) => void;
setStatusBarMessage?: (message: StatusBarMessage | null) => void;
confirmAction: (options: {
title: string;
message: React.ReactNode;
Expand Down Expand Up @@ -1869,7 +1871,7 @@ const CommitListItem: React.FC<CommitListItemProps> = ({ commit, highlight }) =>
);
};

const RepoEditView: React.FC<RepoEditViewProps> = ({ onSave, onCancel, repository, onRefreshState, setToast, confirmAction, defaultCategoryId, onOpenWeblink, detectedExecutables }) => {
const RepoEditView: React.FC<RepoEditViewProps> = ({ onSave, onCancel, repository, onRefreshState, setToast, setStatusBarMessage, confirmAction, defaultCategoryId, onOpenWeblink, detectedExecutables }) => {
const logger = useLogger();
const [formData, setFormData] = useState<Repository | Omit<Repository, 'id'>>(() => repository || NEW_REPO_TEMPLATE);

Expand Down Expand Up @@ -1967,6 +1969,43 @@ const RepoEditView: React.FC<RepoEditViewProps> = ({ 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<ReleaseInfo[] | null>(null);
const [releasesLoading, setReleasesLoading] = useState(false);
Expand Down Expand Up @@ -2035,6 +2074,23 @@ const RepoEditView: React.FC<RepoEditViewProps> = ({ 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;
Expand Down Expand Up @@ -3500,32 +3556,13 @@ const RepoEditView: React.FC<RepoEditViewProps> = ({ onSave, onCancel, repositor
if (!supportsBranchTab) {
return <div className="p-2 text-center text-gray-500">Branch management is only available for Git or SVN repositories.</div>;
}
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 => {
Expand Down Expand Up @@ -3562,15 +3599,17 @@ const RepoEditView: React.FC<RepoEditViewProps> = ({ onSave, onCancel, repositor
<p className="text-sm">
Current branch: <span className="font-bold font-mono text-blue-600 dark:text-blue-400">{branchInfo?.current}</span>
</p>
<div className="max-w-2xl flex flex-col sm:flex-row sm:items-center gap-2">
<input
type="text"
value={branchFilter}
onChange={event => setBranchFilter(event.target.value)}
placeholder="Filter branches"
className={`${formInputStyle} flex-1 min-w-[12rem]`}
/>
<div className="flex flex-wrap items-center gap-2">
<div className="w-full max-w-4xl flex flex-nowrap items-center gap-2">
<div className="flex-1 min-w-0">
<input
type="text"
value={branchFilter}
onChange={event => setBranchFilter(event.target.value)}
placeholder="Filter branches"
className={`${formInputStyle} w-full min-w-[12rem]`}
/>
</div>
<div className="flex flex-nowrap items-center justify-end gap-2 overflow-x-auto flex-shrink-0">
<button
type="button"
onClick={fetchBranches}
Expand Down Expand Up @@ -3606,6 +3645,25 @@ const RepoEditView: React.FC<RepoEditViewProps> = ({ onSave, onCancel, repositor
</button>
</>
)}
{isGitRepo && (
<button
type="button"
onClick={handleBulkDeleteSelectedBranches}
disabled={bulkDeleteDisabled}
title={bulkDeleteTitle}
className={`px-4 py-2 rounded-md text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500 disabled:bg-gray-400 disabled:cursor-not-allowed ${isDeletingBranches ? 'cursor-wait' : ''}`}
>
{isDeletingBranches ? 'Deleting…' : 'Delete Selected'}
</button>
)}
<button
type="button"
onClick={handleCheckoutBranch}
disabled={checkoutDisabled}
className={`px-4 py-2 rounded-md text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:bg-gray-400 disabled:cursor-not-allowed ${isCheckoutLoading ? 'cursor-wait' : ''}`}
>
{isCheckoutLoading ? 'Checking out...' : 'Checkout'}
</button>
</div>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">Tip: Use Shift or Ctrl/Cmd-click to select multiple branches.</p>
Expand Down Expand Up @@ -3696,30 +3754,6 @@ const RepoEditView: React.FC<RepoEditViewProps> = ({ onSave, onCancel, repositor
</div>
</div>
<div className="pt-4 mt-2 border-t border-gray-200 dark:border-gray-700 flex flex-col gap-4 flex-shrink-0">
<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-sm text-gray-600 dark:text-gray-300">{selectionDescription}</p>
<div className="flex flex-wrap items-center gap-2">
{isGitRepo && (
<button
type="button"
onClick={handleBulkDeleteSelectedBranches}
disabled={bulkDeleteDisabled}
title={bulkDeleteTitle}
className={`px-4 py-2 rounded-md text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500 disabled:bg-gray-400 disabled:cursor-not-allowed ${isDeletingBranches ? 'cursor-wait' : ''}`}
>
{isDeletingBranches ? 'Deleting…' : 'Delete Selected'}
</button>
)}
<button
type="button"
onClick={handleCheckoutBranch}
disabled={checkoutDisabled}
className={`px-4 py-2 rounded-md text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 disabled:bg-gray-400 disabled:cursor-not-allowed ${isCheckoutLoading ? 'cursor-wait' : ''}`}
>
{isCheckoutLoading ? 'Checking out...' : 'Checkout'}
</button>
</div>
</div>
{isSvnRepo && (
<p className="text-xs text-gray-500 dark:text-gray-400">
Branch creation, deletion, and merging are currently only available for Git repositories.
Expand Down