feat(UI): Introduce new UI components for file attachments and related views (#191)
Add file management API and UI components Implement directory listing, search and CRUD operations in desktop backend Expose new Files API on frontend to list, search, and modify files
This commit is contained in:
committed by
GitHub
parent
1f23b63c0b
commit
d97181bcd2
@@ -59,7 +59,7 @@ export const FixedSessionsButton: React.FC = () => {
|
||||
}
|
||||
}, [isMobile, setSessionSwitcherOpen, toggleSidebar]);
|
||||
|
||||
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground';
|
||||
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center rounded-md gap-2 p-2 typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground hover:bg-secondary/50 transition-colors';
|
||||
|
||||
if (isMobile || !isDesktopApp || !isMacPlatform) {
|
||||
return null;
|
||||
@@ -89,7 +89,6 @@ export const Header: React.FC = () => {
|
||||
const toggleCommandPalette = useUIStore((state) => state.toggleCommandPalette);
|
||||
const toggleHelpDialog = useUIStore((state) => state.toggleHelpDialog);
|
||||
const isSidebarOpen = useUIStore((state) => state.isSidebarOpen);
|
||||
const sidebarWidth = useUIStore((state) => state.sidebarWidth);
|
||||
const activeMainTab = useUIStore((state) => state.activeMainTab);
|
||||
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
|
||||
|
||||
@@ -168,12 +167,11 @@ export const Header: React.FC = () => {
|
||||
setSettingsDialogOpen(true);
|
||||
}, [blurActiveElement, isMobile, setSessionSwitcherOpen, setSettingsDialogOpen]);
|
||||
|
||||
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground';
|
||||
const headerIconButtonClass = 'app-region-no-drag inline-flex h-9 w-9 items-center justify-center gap-2 p-2 rounded-md typography-ui-label font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50 hover:text-foreground hover:bg-secondary/50 transition-colors';
|
||||
|
||||
const desktopPaddingClass = React.useMemo(() => {
|
||||
if (isDesktopApp && isMacPlatform) {
|
||||
|
||||
return isSidebarOpen ? 'pl-0' : 'pl-[8.0rem]';
|
||||
return isSidebarOpen ? 'pl-2' : 'pl-[8.0rem]';
|
||||
}
|
||||
return 'pl-3';
|
||||
}, [isDesktopApp, isMacPlatform, isSidebarOpen]);
|
||||
@@ -221,15 +219,12 @@ export const Header: React.FC = () => {
|
||||
}, [updateHeaderHeight, isMobile]);
|
||||
|
||||
const handleDragStart = React.useCallback(async (e: React.MouseEvent) => {
|
||||
|
||||
if ((e.target as HTMLElement).closest('button, a, input, select, textarea')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDesktopApp) {
|
||||
try {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
@@ -242,11 +237,9 @@ export const Header: React.FC = () => {
|
||||
}, [isDesktopApp]);
|
||||
|
||||
const handleActiveTabDragStart = React.useCallback(async (e: React.MouseEvent) => {
|
||||
|
||||
if (e.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDesktopApp) {
|
||||
try {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
@@ -278,7 +271,6 @@ export const Header: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
|
||||
if (hasModifier(e) && !e.shiftKey && !e.altKey) {
|
||||
const num = parseInt(e.key, 10);
|
||||
if (num >= 1 && num <= tabs.length) {
|
||||
@@ -287,82 +279,58 @@ export const Header: React.FC = () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [tabs, setActiveMainTab]);
|
||||
|
||||
const renderTab = (tab: TabConfig, isLast: boolean) => {
|
||||
const renderTab = (tab: TabConfig) => {
|
||||
const isActive = activeMainTab === tab.id;
|
||||
const Icon = tab.icon;
|
||||
const isChatTab = tab.id === 'chat';
|
||||
const isGitTab = tab.id === 'git';
|
||||
|
||||
return (
|
||||
<React.Fragment key={tab.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveMainTab(tab.id)}
|
||||
onMouseDown={isActive ? handleActiveTabDragStart : undefined}
|
||||
className={cn(
|
||||
'relative flex h-full items-center gap-2 px-4 typography-ui-label font-medium transition-colors',
|
||||
isActive ? 'app-region-drag' : 'app-region-no-drag',
|
||||
'hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary',
|
||||
isActive ? 'text-foreground' : 'text-muted-foreground',
|
||||
|
||||
isActive && 'after:absolute after:bottom-[-1px] after:left-0 after:right-0 after:h-[2px] after:bg-background',
|
||||
|
||||
isActive && isChatTab && isSidebarOpen && 'before:absolute before:bottom-[-1px] before:right-full before:h-px before:bg-[var(--interactive-border)] before:w-[var(--sidebar-w)]',
|
||||
|
||||
isChatTab && !(isDesktopApp && isMacPlatform && isSidebarOpen) && 'border-l',
|
||||
|
||||
isGitTab && 'border-r',
|
||||
|
||||
isChatTab && !isMobile && 'min-w-[165px] max-[1024px]:min-w-0'
|
||||
)}
|
||||
style={{
|
||||
...(isActive && isChatTab && isSidebarOpen ? {
|
||||
|
||||
['--sidebar-w' as string]: (isDesktopApp && isMacPlatform) ? `${sidebarWidth}px` : '64px'
|
||||
} : {}),
|
||||
...((isChatTab && !(isDesktopApp && isMacPlatform && isSidebarOpen)) || isGitTab ? { borderColor: 'var(--interactive-border)' } : {}),
|
||||
}}
|
||||
aria-label={tab.label}
|
||||
aria-selected={isActive}
|
||||
role="tab"
|
||||
>
|
||||
{isMobile ? (
|
||||
<Icon size={20} />
|
||||
) : (
|
||||
<>
|
||||
<Icon size={16} />
|
||||
<span className="header-tab-label">{tab.label}</span>
|
||||
</>
|
||||
)}
|
||||
{}
|
||||
{isChatTab && !isMobile && contextUsage && contextUsage.totalTokens > 0 && (
|
||||
<span className="ml-1">
|
||||
<ContextUsageDisplay
|
||||
totalTokens={contextUsage.totalTokens}
|
||||
percentage={contextUsage.percentage}
|
||||
contextLimit={contextUsage.contextLimit}
|
||||
outputLimit={contextUsage.outputLimit ?? 0}
|
||||
size="compact"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{}
|
||||
{tab.badge !== undefined && tab.badge > 0 && (
|
||||
<span className="text-xs font-semibold text-primary">
|
||||
{tab.badge}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
{}
|
||||
{!isLast && (
|
||||
<div className="h-full w-px bg-border" aria-hidden="true" />
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
onClick={() => setActiveMainTab(tab.id)}
|
||||
onMouseDown={isActive ? handleActiveTabDragStart : undefined}
|
||||
className={cn(
|
||||
'relative flex h-8 items-center gap-2 px-3 rounded-md typography-ui-label font-medium transition-colors',
|
||||
isActive ? 'app-region-drag bg-secondary text-foreground shadow-sm' : 'app-region-no-drag text-muted-foreground hover:bg-secondary/50 hover:text-foreground',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
|
||||
isChatTab && !isMobile && 'min-w-[100px] justify-center'
|
||||
)}
|
||||
</React.Fragment>
|
||||
aria-label={tab.label}
|
||||
aria-selected={isActive}
|
||||
role="tab"
|
||||
>
|
||||
{isMobile ? (
|
||||
<Icon size={20} />
|
||||
) : (
|
||||
<>
|
||||
<Icon size={16} />
|
||||
<span className="header-tab-label">{tab.label}</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isChatTab && !isMobile && contextUsage && contextUsage.totalTokens > 0 && (
|
||||
<span className="ml-1">
|
||||
<ContextUsageDisplay
|
||||
totalTokens={contextUsage.totalTokens}
|
||||
percentage={contextUsage.percentage}
|
||||
contextLimit={contextUsage.contextLimit}
|
||||
outputLimit={contextUsage.outputLimit ?? 0}
|
||||
size="compact"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{tab.badge !== undefined && tab.badge > 0 && (
|
||||
<span className="ml-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary/10 px-1 text-[10px] font-bold text-primary">
|
||||
{tab.badge}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -376,29 +344,25 @@ export const Header: React.FC = () => {
|
||||
role="tablist"
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
{}
|
||||
{!(isDesktopApp && isMacPlatform) && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenSessionSwitcher}
|
||||
aria-label="Open sessions"
|
||||
className={`${headerIconButtonClass} mr-2.5`}
|
||||
className={`${headerIconButtonClass} mr-2`}
|
||||
>
|
||||
<RiLayoutLeftLine className="h-5 w-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{}
|
||||
<div className="flex h-full items-center">
|
||||
{tabs.map((tab, index) => renderTab(tab, index === tabs.length - 1))}
|
||||
<div className="flex items-center gap-1 p-1 bg-background/50 rounded-lg">
|
||||
{tabs.map((tab) => renderTab(tab))}
|
||||
</div>
|
||||
|
||||
{}
|
||||
<div className="flex-1" />
|
||||
|
||||
{}
|
||||
<div className="flex items-center gap-1 pr-3">
|
||||
<Tooltip delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -442,7 +406,7 @@ export const Header: React.FC = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleOpenSessionSwitcher}
|
||||
className="app-region-no-drag h-9 w-9 p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
className="app-region-no-drag h-9 w-9 p-2 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-md active:bg-secondary"
|
||||
aria-label="Open sessions"
|
||||
>
|
||||
<RiPlayListAddLine className="h-5 w-5" />
|
||||
@@ -459,9 +423,10 @@ export const Header: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="app-region-no-drag flex items-center gap-1.5">
|
||||
<div className="app-region-no-drag flex items-center gap-1">
|
||||
|
||||
<div className="flex items-center gap-0.5" role="tablist" aria-label="Main navigation">
|
||||
|
||||
<div className="flex items-center" role="tablist" aria-label="Main navigation">
|
||||
{tabs.map((tab) => {
|
||||
const isActive = activeMainTab === tab.id;
|
||||
const Icon = tab.icon;
|
||||
@@ -482,7 +447,7 @@ export const Header: React.FC = () => {
|
||||
className={cn(
|
||||
headerIconButtonClass,
|
||||
'relative',
|
||||
isActive && 'text-foreground'
|
||||
isActive && 'text-foreground bg-secondary'
|
||||
)}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
@@ -535,7 +500,7 @@ export const Header: React.FC = () => {
|
||||
);
|
||||
|
||||
const headerClassName = cn(
|
||||
'header-safe-area border-b relative z-10',
|
||||
'header-safe-area border-b border-border/50 relative z-10',
|
||||
isDesktopApp ? 'bg-background' : 'bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80'
|
||||
);
|
||||
|
||||
@@ -543,7 +508,7 @@ export const Header: React.FC = () => {
|
||||
<header
|
||||
ref={headerRef}
|
||||
className={headerClassName}
|
||||
style={{ borderColor: 'var(--interactive-border)', ['--padding-scale' as string]: '1' } as React.CSSProperties}
|
||||
style={{ ['--padding-scale' as string]: '1' } as React.CSSProperties}
|
||||
>
|
||||
{isMobile ? renderMobile() : renderDesktop()}
|
||||
</header>
|
||||
|
||||
@@ -168,19 +168,17 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
'relative flex h-full overflow-hidden border-r',
|
||||
'relative flex h-full overflow-hidden border-r border-border',
|
||||
isDesktopApp
|
||||
? 'bg-[color:var(--sidebar-overlay-strong)] backdrop-blur supports-[backdrop-filter]:bg-[color:var(--sidebar-overlay-soft)]'
|
||||
? 'bg-sidebar/95 backdrop-blur supports-[backdrop-filter]:bg-sidebar/80'
|
||||
: 'bg-sidebar',
|
||||
isResizing ? 'transition-none' : '',
|
||||
isResizing ? 'transition-none' : 'transition-[width] duration-300 ease-in-out',
|
||||
!isOpen && 'border-r-0'
|
||||
)}
|
||||
style={{
|
||||
width: `${appliedWidth}px`,
|
||||
minWidth: `${appliedWidth}px`,
|
||||
maxWidth: `${appliedWidth}px`,
|
||||
pointerEvents: !isOpen ? 'none' : undefined,
|
||||
borderColor: 'var(--interactive-border)',
|
||||
overflowX: 'clip',
|
||||
}}
|
||||
aria-hidden={!isOpen || appliedWidth === 0}
|
||||
@@ -188,8 +186,8 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
|
||||
{isOpen && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute right-0 top-0 z-20 h-full w-[6px] -mr-[3px] cursor-col-resize',
|
||||
isResizing ? 'bg-primary/30' : 'bg-transparent hover:bg-primary/20'
|
||||
'absolute right-0 top-0 z-20 h-full w-[4px] cursor-col-resize hover:bg-primary/50 transition-colors',
|
||||
isResizing && 'bg-primary'
|
||||
)}
|
||||
onPointerDown={handlePointerDown}
|
||||
role="separator"
|
||||
@@ -199,7 +197,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-10 flex h-full flex-col transition-opacity duration-200 ease-in-out',
|
||||
'relative z-10 flex h-full flex-col transition-opacity duration-300 ease-in-out',
|
||||
!isOpen && 'pointer-events-none select-none opacity-0'
|
||||
)}
|
||||
style={{ width: `${appliedWidth}px`, overflowX: 'hidden' }}
|
||||
@@ -216,44 +214,45 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</div>
|
||||
<div className="flex-shrink-0 border-t border-border p-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex-shrink-0 border-t border-border h-12 px-2 bg-sidebar-accent/10">
|
||||
<div className="flex h-full items-center justify-between gap-2">
|
||||
<button
|
||||
onClick={() => setSettingsDialogOpen(true)}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md px-3 py-2',
|
||||
'text-sm font-semibold text-muted-foreground',
|
||||
'hover:text-foreground',
|
||||
'transition-colors'
|
||||
'flex h-8 items-center gap-2 rounded-md px-2',
|
||||
'text-sm font-semibold text-sidebar-foreground/90',
|
||||
'hover:text-sidebar-foreground hover:bg-sidebar-accent',
|
||||
'transition-all duration-200'
|
||||
)}
|
||||
>
|
||||
<RiSettings3Line className="h-4 w-4" />
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
{(available || downloaded) ? (
|
||||
<button
|
||||
onClick={() => setUpdateDialogOpen(true)}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-md px-2.5 py-1.5',
|
||||
'text-xs font-medium',
|
||||
'bg-primary/10 text-primary',
|
||||
'hover:bg-primary/20',
|
||||
'transition-colors'
|
||||
)}
|
||||
>
|
||||
<RiDownloadLine className="h-3.5 w-3.5" />
|
||||
<span>Update</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setUpdateDialogOpen(true)}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-md px-2 py-1',
|
||||
'text-xs font-semibold',
|
||||
'bg-primary/10 text-primary',
|
||||
'hover:bg-primary/20',
|
||||
'transition-colors'
|
||||
)}
|
||||
>
|
||||
<RiDownloadLine className="h-3.5 w-3.5" />
|
||||
<span>Update</span>
|
||||
</button>
|
||||
|
||||
) : !isDesktopApp && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => setAboutDialogOpen(true)}
|
||||
className={cn(
|
||||
'flex items-center justify-center rounded-md p-1.5',
|
||||
'text-muted-foreground',
|
||||
'hover:text-foreground hover:bg-muted/50',
|
||||
'transition-colors'
|
||||
'flex h-8 w-8 items-center justify-center rounded-md',
|
||||
'text-sidebar-foreground/70',
|
||||
'hover:text-sidebar-foreground hover:bg-sidebar-accent',
|
||||
'transition-all duration-200'
|
||||
)}
|
||||
>
|
||||
<RiInformationLine className="h-4 w-4" />
|
||||
|
||||
Reference in New Issue
Block a user