The composer rendered a second StatusRow instance carrying the pending-changes bar and the todos dropdown, so every restyle of the floating assistant-status chip (glass, placement, sizing) silently restyled the composer bar and its dropdown too — for the fourth time. StatusRow is now only the floating chip above the composer; the composer's own bar is a new ComposerStatusBar with the pre-glass layout, its own container name, and its own container-query classes (the anti-overlap rules that hide the long active todo under 38rem and the changed-files label under 30rem moved with it — losing them during the split let the two dropdown triggers overlap on mobile). The two components share no markup and no CSS hooks anymore.
148 lines
6.0 KiB
TypeScript
148 lines
6.0 KiB
TypeScript
import React from 'react';
|
|
import { useGitStore, useIsGitRepo } from '@/stores/useGitStore';
|
|
import { useUIStore } from '@/stores/useUIStore';
|
|
import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext';
|
|
import { useMobileAppActions } from '@/apps/mobileAppContext';
|
|
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
|
import { Icon } from "@/components/icon/Icon";
|
|
import { cn } from '@/lib/utils';
|
|
import {
|
|
type ChangedFileEntry,
|
|
type GitChangedFile,
|
|
extractGitChangedFiles,
|
|
isGitFile,
|
|
} from './changedFiles';
|
|
import { ChangedFilesList } from './ChangedFilesList';
|
|
import { changedFilesPopoverClassName, changedFilesPopoverStyle } from './changedFilesPopover';
|
|
import { useI18n } from '@/lib/i18n';
|
|
|
|
export const PendingChangesBar: React.FC = React.memo(() => {
|
|
const { t } = useI18n();
|
|
const [isExpanded, setIsExpanded] = React.useState(false);
|
|
const popoverRef = React.useRef<HTMLDivElement>(null);
|
|
const currentDirectory = useEffectiveDirectory() ?? null;
|
|
const runtime = React.useContext(RuntimeAPIContext);
|
|
const isGitRepo = useIsGitRepo(currentDirectory);
|
|
const gitStatus = useGitStore((s) =>
|
|
currentDirectory ? s.directories.get(currentDirectory)?.status ?? null : null,
|
|
);
|
|
const mobileActions = useMobileAppActions();
|
|
|
|
// Close popover when clicking outside
|
|
React.useEffect(() => {
|
|
if (!isExpanded) return;
|
|
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (popoverRef.current && !popoverRef.current.contains(event.target as Node)) {
|
|
setIsExpanded(false);
|
|
}
|
|
};
|
|
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
}, [isExpanded]);
|
|
|
|
const gitChangedFiles = React.useMemo<GitChangedFile[]>(() => {
|
|
if (!currentDirectory || isGitRepo !== true || !gitStatus || gitStatus.isClean) return [];
|
|
return extractGitChangedFiles(gitStatus.files, gitStatus.diffStats, currentDirectory);
|
|
}, [isGitRepo, gitStatus, currentDirectory]);
|
|
|
|
const { totalAdded, totalRemoved } = React.useMemo(() => {
|
|
let added = 0;
|
|
let removed = 0;
|
|
for (const file of gitChangedFiles) {
|
|
added += file.insertions;
|
|
removed += file.deletions;
|
|
}
|
|
return { totalAdded: added, totalRemoved: removed };
|
|
}, [gitChangedFiles]);
|
|
|
|
if (!currentDirectory || isGitRepo !== true) return null;
|
|
if (gitChangedFiles.length === 0) return null;
|
|
|
|
const handleOpenFile = (file: ChangedFileEntry) => {
|
|
if (!currentDirectory) return;
|
|
if (!isGitFile(file)) return;
|
|
|
|
setIsExpanded(false);
|
|
|
|
const absolutePath = file.path;
|
|
|
|
// Dedicated mobile root: open the per-file diff inside the mobile Changes surface.
|
|
if (mobileActions) {
|
|
mobileActions.openChanges({
|
|
diffPath: file.relativePath,
|
|
staged: file.hasStagedChanges && !file.hasWorkingChanges,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const editor = runtime?.editor;
|
|
if (editor) {
|
|
void editor.openFile(absolutePath);
|
|
return;
|
|
}
|
|
|
|
const store = useUIStore.getState();
|
|
const openStagedDiff = file.hasStagedChanges && !file.hasWorkingChanges;
|
|
if (!store.isMobile) {
|
|
store.openContextDiff(currentDirectory, file.relativePath, openStagedDiff);
|
|
return;
|
|
}
|
|
store.navigateToDiff(file.relativePath, openStagedDiff);
|
|
};
|
|
|
|
const fileCount = gitChangedFiles.length;
|
|
const labelHead = fileCount === 1
|
|
? t('chat.pendingChanges.fileCountSingle', { count: fileCount })
|
|
: t('chat.pendingChanges.fileCountPlural', { count: fileCount });
|
|
|
|
return (
|
|
<div className="relative" ref={popoverRef}>
|
|
<button
|
|
type="button"
|
|
className="flex min-w-0 max-w-full items-center gap-1 text-left text-muted-foreground"
|
|
onClick={() => setIsExpanded((value) => !value)}
|
|
aria-expanded={isExpanded}
|
|
>
|
|
<Icon name="file-edit" className="h-3.5 w-3.5 flex-shrink-0 text-[var(--status-warning)]" />
|
|
<span className="min-w-0 typography-ui-label text-foreground flex-shrink-0">{labelHead}</span>
|
|
<span className="composer-status-bar__changed-label min-w-0 typography-ui-label text-foreground truncate">
|
|
{t('chat.pendingChanges.changedInWorkspace')}
|
|
</span>
|
|
<span className="text-[0.75rem] tabular-nums inline-flex items-baseline gap-1 flex-shrink-0">
|
|
{totalAdded > 0 ? <span style={{ color: 'var(--status-success)' }}>+{totalAdded}</span> : null}
|
|
{totalRemoved > 0 ? <span style={{ color: 'var(--status-error)' }}>-{totalRemoved}</span> : null}
|
|
</span>
|
|
{isExpanded ? (
|
|
<Icon name="arrow-up-s" className="h-3.5 w-3.5 flex-shrink-0" />
|
|
) : (
|
|
<Icon name="arrow-down-s" className="h-3.5 w-3.5 flex-shrink-0" />
|
|
)}
|
|
</button>
|
|
{isExpanded && (
|
|
<div
|
|
style={{
|
|
...changedFilesPopoverStyle,
|
|
maxWidth: 'min(28rem, calc(100cqw - 4ch))',
|
|
}}
|
|
className={cn(
|
|
changedFilesPopoverClassName,
|
|
"absolute left-0 bottom-full mb-1 z-50",
|
|
"animate-in fade-in-0 zoom-in-95 slide-in-from-bottom-2",
|
|
"duration-150"
|
|
)}
|
|
>
|
|
<ChangedFilesList
|
|
files={gitChangedFiles}
|
|
currentDirectory={currentDirectory}
|
|
onOpenFile={handleOpenFile}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
PendingChangesBar.displayName = 'PendingChangesBar';
|