refactor(diff): enchance file list display

This commit is contained in:
Bohdan Triapitsyn
2026-01-14 15:21:44 +02:00
parent e95bfdebaf
commit be5a12af6e
4 changed files with 82 additions and 73 deletions
+6 -6
View File
@@ -74,7 +74,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@openchamber/desktop", "name": "@openchamber/desktop",
"version": "1.4.6", "version": "1.4.8",
"dependencies": { "dependencies": {
"@openchamber/ui": "workspace:*", "@openchamber/ui": "workspace:*",
"@tauri-apps/plugin-notification": "^2.3.3", "@tauri-apps/plugin-notification": "^2.3.3",
@@ -97,7 +97,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@openchamber/ui", "name": "@openchamber/ui",
"version": "1.4.6", "version": "1.4.8",
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
@@ -106,7 +106,7 @@
"@fontsource/ibm-plex-sans": "^5.1.1", "@fontsource/ibm-plex-sans": "^5.1.1",
"@ibm/plex": "^6.4.1", "@ibm/plex": "^6.4.1",
"@opencode-ai/sdk": "^1.1.8", "@opencode-ai/sdk": "^1.1.8",
"@pierre/diffs": "^1.0.0", "@pierre/diffs": "^1.0.5",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -169,7 +169,7 @@
}, },
"packages/vscode": { "packages/vscode": {
"name": "openchamber", "name": "openchamber",
"version": "1.4.6", "version": "1.4.8",
"dependencies": { "dependencies": {
"@openchamber/ui": "workspace:*", "@openchamber/ui": "workspace:*",
"@opencode-ai/sdk": "^1.1.8", "@opencode-ai/sdk": "^1.1.8",
@@ -192,7 +192,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@openchamber/web", "name": "@openchamber/web",
"version": "1.4.6", "version": "1.4.8",
"bin": { "bin": {
"openchamber": "./bin/cli.js", "openchamber": "./bin/cli.js",
}, },
@@ -539,7 +539,7 @@
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.8", "", {}, "sha512-N/02945tafTFN1FjlGCWO++dErtNyWaYOdARPifuL4nSsxTk2XtqZ2naZ28rj8kekjYR8JX1MA5upNEqwztOZA=="], "@opencode-ai/sdk": ["@opencode-ai/sdk@1.1.8", "", {}, "sha512-N/02945tafTFN1FjlGCWO++dErtNyWaYOdARPifuL4nSsxTk2XtqZ2naZ28rj8kekjYR8JX1MA5upNEqwztOZA=="],
"@pierre/diffs": ["@pierre/diffs@1.0.2", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "3.19.0", "@shikijs/transformers": "3.19.0", "diff": "8.0.2", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "3.19.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-RkFSDD5X/U+8QjyilPViYGJfmJNWXR17zTL8zw48+DcVC1Ujbh6I1edyuRnFfgRzpft05x2DSCkz2cjoIAxPvQ=="], "@pierre/diffs": ["@pierre/diffs@1.0.5", "", { "dependencies": { "@shikijs/core": "^3.0.0", "@shikijs/engine-javascript": "^3.0.0", "@shikijs/transformers": "^3.0.0", "diff": "8.0.2", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-QcFhO6BW1Zz3BP+WFuH1tO2DjFJY5Sb6NmRjmEoVeEu1AVOd3HoUEFTnztxgxn+2c2ZFyFZP+6T4X/g8LDuZLw=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
+1 -1
View File
@@ -2977,7 +2977,7 @@ dependencies = [
[[package]] [[package]]
name = "openchamber-desktop" name = "openchamber-desktop"
version = "1.4.7" version = "1.4.8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
+1 -1
View File
@@ -18,7 +18,7 @@
"@fontsource/ibm-plex-sans": "^5.1.1", "@fontsource/ibm-plex-sans": "^5.1.1",
"@ibm/plex": "^6.4.1", "@ibm/plex": "^6.4.1",
"@opencode-ai/sdk": "^1.1.8", "@opencode-ai/sdk": "^1.1.8",
"@pierre/diffs": "^1.0.0", "@pierre/diffs": "^1.0.5",
"@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
+74 -65
View File
@@ -5,6 +5,7 @@ import { useSessionStore } from '@/stores/useSessionStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useUIStore } from '@/stores/useUIStore'; import { useUIStore } from '@/stores/useUIStore';
import { useGitStore, useGitStatus, useIsGitRepo, useGitFileCount } from '@/stores/useGitStore'; import { useGitStore, useGitStatus, useIsGitRepo, useGitFileCount } from '@/stores/useGitStore';
import { cn } from '@/lib/utils';
import type { GitStatus } from '@/lib/api/types'; import type { GitStatus } from '@/lib/api/types';
import { import {
DropdownMenu, DropdownMenu,
@@ -15,6 +16,7 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { getLanguageFromExtension, isImageFile } from '@/lib/toolHelpers'; import { getLanguageFromExtension, isImageFile } from '@/lib/toolHelpers';
@@ -240,21 +242,13 @@ interface FileListProps {
changedFiles: FileEntry[]; changedFiles: FileEntry[];
selectedFile: string | null; selectedFile: string | null;
onSelectFile: (path: string) => void; onSelectFile: (path: string) => void;
isCompact: boolean;
} }
const FileList = React.memo<FileListProps>(({ const FileList = React.memo<FileListProps>(({
changedFiles, changedFiles,
selectedFile, selectedFile,
onSelectFile, onSelectFile,
isCompact,
}) => { }) => {
const getLabel = React.useCallback((path: string) => {
if (!isCompact) return path;
const lastSlash = path.lastIndexOf('/');
return lastSlash >= 0 ? path.slice(lastSlash + 1) : path;
}, [isCompact]);
if (changedFiles.length === 0) return null; if (changedFiles.length === 0) return null;
return ( return (
@@ -269,11 +263,12 @@ const FileList = React.memo<FileListProps>(({
<button <button
type="button" type="button"
onClick={() => onSelectFile(file.path)} onClick={() => onSelectFile(file.path)}
className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors ${ className={cn(
'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors',
isActive isActive
? 'bg-accent/70 text-foreground' ? 'bg-accent/70 text-foreground'
: 'text-muted-foreground hover:bg-accent/40 hover:text-foreground' : 'text-muted-foreground hover:bg-accent/40 hover:text-foreground'
}`} )}
> >
<span <span
className="typography-micro font-semibold w-4 text-center uppercase" className="typography-micro font-semibold w-4 text-center uppercase"
@@ -285,9 +280,10 @@ const FileList = React.memo<FileListProps>(({
</span> </span>
<span <span
className="min-w-0 flex-1 truncate typography-meta" className="min-w-0 flex-1 truncate typography-meta"
style={{ direction: 'rtl', textAlign: 'left' }}
title={file.path} title={file.path}
> >
{getLabel(file.path)} {file.path}
</span> </span>
{formatDiffTotals(file.insertions, file.deletions)} {formatDiffTotals(file.insertions, file.deletions)}
</button> </button>
@@ -701,50 +697,64 @@ const MultiFileDiffEntry = React.memo<MultiFileDiffEntryProps>(({
<Collapsible <Collapsible
open={isExpanded} open={isExpanded}
onOpenChange={handleOpenChange} onOpenChange={handleOpenChange}
className="rounded-xl border border-border/60 bg-background/70 overflow-hidden" className="group/collapsible"
> >
<CollapsibleTrigger <div className="sticky top-0 z-10 bg-background">
onClick={handleSelect} <CollapsibleTrigger
className={`group gap-2 ${isSelected ? 'bg-accent/60' : ''}`} onClick={handleSelect}
> className={cn(
<div className="flex min-w-0 flex-1 items-center gap-2"> 'relative flex w-full items-center gap-2 px-3 py-1.5 transition-colors rounded-t-xl border border-border/60 overflow-hidden',
<span className="flex size-5 items-center justify-center text-muted-foreground"> 'bg-background hover:bg-background',
{isExpanded ? ( isExpanded ? 'rounded-b-none' : 'rounded-b-xl',
<RiArrowDownSLine className="size-4" /> isSelected
) : ( ? 'text-primary'
<RiArrowRightSLine className="size-4" /> : 'text-muted-foreground hover:text-foreground'
)} )}
</span> >
<span <div className={cn(
className="typography-micro font-semibold w-4 text-center uppercase" 'absolute inset-0 pointer-events-none transition-colors',
style={{ color: descriptor.color }} isSelected ? 'bg-primary/10' : 'group-hover:bg-accent/40'
title={descriptor.description} )} />
aria-label={descriptor.description} <div className="relative flex min-w-0 flex-1 items-center gap-2">
> <span className="flex size-5 items-center justify-center opacity-70 group-hover:opacity-100 transition-opacity">
{descriptor.code} {isExpanded ? (
</span> <RiArrowDownSLine className="size-4" />
<span ) : (
className="min-w-0 flex-1 truncate typography-ui-label" <RiArrowRightSLine className="size-4" />
title={file.path} )}
> </span>
{file.path} <span
</span> className="typography-micro font-semibold w-4 text-center uppercase"
</div> style={{ color: descriptor.color }}
<div className="flex items-center gap-2"> title={descriptor.description}
{formatDiffTotals(file.insertions, file.deletions)} aria-label={descriptor.description}
<DiffViewToggle >
mode={renderSideBySide ? 'side-by-side' : 'unified'} {descriptor.code}
onModeChange={(mode: DiffViewMode) => { </span>
const nextLayout: 'inline' | 'side-by-side' = <span
mode === 'side-by-side' ? 'side-by-side' : 'inline'; className="min-w-0 flex-1 truncate typography-ui-label"
setDiffFileLayout(file.path, nextLayout); style={{ direction: 'rtl', textAlign: 'left' }}
}} title={file.path}
className="opacity-70" >
/> {file.path}
</div> </span>
</CollapsibleTrigger> </div>
<CollapsibleContent className="border-t border-border/60 bg-background"> <div className="relative flex items-center gap-2">
<div className="relative"> {formatDiffTotals(file.insertions, file.deletions)}
<DiffViewToggle
mode={renderSideBySide ? 'side-by-side' : 'unified'}
onModeChange={(mode: DiffViewMode) => {
const nextLayout: 'inline' | 'side-by-side' =
mode === 'side-by-side' ? 'side-by-side' : 'inline';
setDiffFileLayout(file.path, nextLayout);
}}
className="opacity-70"
/>
</div>
</CollapsibleTrigger>
</div>
<CollapsibleContent>
<div className="relative border border-t-0 border-border/60 bg-background rounded-b-xl overflow-hidden">
{diffLoadError ? ( {diffLoadError ? (
<div className="flex flex-col items-center gap-2 px-4 py-8 text-sm text-muted-foreground"> <div className="flex flex-col items-center gap-2 px-4 py-8 text-sm text-muted-foreground">
<div className="typography-ui-label font-semibold text-foreground"> <div className="typography-ui-label font-semibold text-foreground">
@@ -1065,7 +1075,7 @@ export const DiffView: React.FC = () => {
<div className="flex flex-1 min-h-0 gap-3 px-3 pb-3 pt-2"> <div className="flex flex-1 min-h-0 gap-3 px-3 pb-3 pt-2">
{showFileSidebar && ( {showFileSidebar && (
<section className="hidden lg:flex w-72 flex-col rounded-xl border border-border/60 bg-background/70 overflow-hidden"> <section className="hidden lg:flex w-72 flex-col rounded-xl border border-border/60 bg-background/70 overflow-hidden">
<div className="flex items-center justify-between px-3 py-2 border-b border-border/40"> <div className="flex items-center justify-between px-3 py-1.5 border-b border-border/40">
<span className="typography-ui-header font-semibold text-foreground">Files</span> <span className="typography-ui-header font-semibold text-foreground">Files</span>
<span className="typography-meta text-muted-foreground">{changedFiles.length}</span> <span className="typography-meta text-muted-foreground">{changedFiles.length}</span>
</div> </div>
@@ -1073,7 +1083,6 @@ export const DiffView: React.FC = () => {
changedFiles={changedFiles} changedFiles={changedFiles}
selectedFile={selectedFile} selectedFile={selectedFile}
onSelectFile={handleSelectFileAndScroll} onSelectFile={handleSelectFileAndScroll}
isCompact={screenWidth < 1280}
/> />
</section> </section>
)} )}
@@ -1082,7 +1091,7 @@ export const DiffView: React.FC = () => {
outerClassName="flex-1 min-h-0" outerClassName="flex-1 min-h-0"
className="pr-2" className="pr-2"
> >
<div className="flex flex-col gap-3 py-1"> <div className="flex flex-col gap-3">
{changedFiles.map((file) => ( {changedFiles.map((file) => (
<MultiFileDiffEntry <MultiFileDiffEntry
key={file.path} key={file.path}
@@ -1207,18 +1216,18 @@ export const DiffView: React.FC = () => {
)} )}
<div className="flex-1" /> <div className="flex-1" />
{selectedFileEntry && ( {selectedFileEntry && (
<button <Button
type="button" variant="ghost"
size="sm"
onClick={() => setDiffWrapLines(!diffWrapLinesStore)} onClick={() => setDiffWrapLines(!diffWrapLinesStore)}
className={`flex items-center justify-center size-5 rounded-sm transition-opacity ${ className={cn(
diffWrapLines 'h-5 w-5 p-0 transition-opacity',
? 'text-foreground opacity-100' diffWrapLines ? 'text-foreground opacity-100' : 'text-muted-foreground opacity-60 hover:opacity-100'
: 'text-muted-foreground opacity-60 hover:opacity-100' )}
}`}
title={diffWrapLines ? 'Disable line wrap' : 'Enable line wrap'} title={diffWrapLines ? 'Disable line wrap' : 'Enable line wrap'}
> >
<RiTextWrap className="size-4" /> <RiTextWrap className="size-4" />
</button> </Button>
)} )}
{selectedFileEntry && currentLayoutForSelectedFile && ( {selectedFileEntry && currentLayoutForSelectedFile && (
<DiffViewToggle <DiffViewToggle