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:
Bohdan Triapitsyn
2026-01-22 12:21:19 +02:00
committed by GitHub
parent 1f23b63c0b
commit d97181bcd2
22 changed files with 1611 additions and 1223 deletions
@@ -221,7 +221,7 @@ export const ChatContainer: React.FC = () => {
style={isMobile ? { paddingBottom: 'var(--oc-keyboard-inset, 0px)' } : undefined}
>
<div className="flex-1 overflow-y-auto p-4 bg-background">
<div className="chat-column space-y-4">
<div className="chat-message-column space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="flex gap-3 p-4">
<Skeleton className="h-8 w-8 rounded-full" />
@@ -827,17 +827,17 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
<div
className={cn(
'group w-full',
shouldShowHeader ? 'pt-2' : 'pt-0',
isUser ? 'pb-2' : isFollowedByAssistant ? 'pb-0' : 'pb-2'
shouldShowHeader ? 'pt-6' : 'pt-0',
isUser ? 'pb-4' : isFollowedByAssistant ? 'pb-0' : 'pb-8'
)}
data-message-id={message.info.id}
ref={messageContainerRef}
>
<div className="chat-column">
<div className="chat-message-column relative">
{isUser ? (
<FadeInOnReveal>
<div className="flex justify-end">
<div className="max-w-[85%] rounded-xl rounded-br-xs bg-primary/10 dark:bg-primary/8 px-3.5 pt-2.5 pb-1.5">
<div className="max-w-[85%] rounded-2xl rounded-br-sm bg-primary/10 dark:bg-primary/10 px-5 py-3 shadow-sm border border-primary/5">
<MessageBody
messageId={message.info.id}
parts={visibleParts}
@@ -870,7 +870,7 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
</div>
</FadeInOnReveal>
) : (
<div>
<div className="relative pl-4 ml-1">
{shouldShowHeader && (
<MessageHeader
isUser={isUser}
@@ -168,9 +168,11 @@ const FileChip = memo(({ file, onRemove }: FileChipProps) => {
)}
</div>
{getFileIcon()}
<span title={file.serverPath || displayName}>
{displayName}
</span>
<div className="overflow-hidden max-w-[200px]">
<span className="marquee-text" title={file.serverPath || displayName}>
{displayName}
</span>
</div>
<span className="text-muted-foreground flex-shrink-0">
({formatFileSize(file.size)})
</span>
@@ -289,9 +291,11 @@ export const MessageFilesDisplay = memo(({ files, onShowPopup }: MessageFilesDis
className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-muted/30 border border-border/30 rounded-xl typography-meta"
>
{getFileIcon(file.mime)}
<span>
{extractFilename(file.filename)}
</span>
<div className="overflow-hidden max-w-[200px]">
<span className="marquee-text">
{extractFilename(file.filename)}
</span>
</div>
</div>
))}
</div>
@@ -2049,13 +2049,15 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
<span
key={`${currentProviderId}-${currentModelId}`}
className={cn(
'model-controls__model-label',
'model-controls__model-label overflow-hidden',
controlTextSize,
'font-medium whitespace-nowrap text-foreground truncate min-w-0',
'font-medium whitespace-nowrap text-foreground min-w-0',
'max-w-[260px]'
)}
>
{getCurrentModelDisplayName()}
<span className="marquee-text">
{getCurrentModelDisplayName()}
</span>
</span>
</div>
</DropdownMenuTrigger>
@@ -2169,11 +2171,13 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
)}
<span
className={cn(
'model-controls__model-label typography-micro font-medium truncate min-w-0',
'model-controls__model-label typography-micro font-medium overflow-hidden min-w-0',
isMobile ? 'max-w-[120px]' : 'max-w-[220px]',
)}
>
{getCurrentModelDisplayName()}
<span className="marquee-text">
{getCurrentModelDisplayName()}
</span>
</span>
</button>
)}
@@ -155,7 +155,7 @@ const UserMessageBody: React.FC<{
style={{ contain: 'layout', transform: 'translateZ(0)' }}
onTouchStart={isTouchContext && canCopyMessage && hasCopyableText ? revealCopyHint : undefined}
>
<div className="leading-normal overflow-hidden text-foreground/85">
<div className="leading-relaxed overflow-hidden text-foreground/90 text-base">
{textParts.map((part, index) => {
let mentionForPart: AgentMentionInfo | undefined;
if (agentMention && mentionToken && !mentionInjected) {
@@ -180,8 +180,7 @@ const UserMessageBody: React.FC<{
<MessageFilesDisplay files={parts} onShowPopup={onShowPopup} />
{(canCopyMessage && hasCopyableText) || onRevert || onFork ? (
<div className={cn(
"mt-1 flex items-center justify-end gap-2 opacity-0 pointer-events-none transition-opacity duration-150 group-hover/message:opacity-100 group-hover/message:pointer-events-auto focus-within:opacity-100 focus-within:pointer-events-auto",
copyHintVisible && "opacity-100 pointer-events-auto"
"mt-1 flex items-center justify-end gap-2"
)}>
{onRevert && (
<Tooltip delayDuration={1000}>
@@ -284,7 +283,6 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
errorMessage,
}) => {
void _streamPhase;
void _allowAnimation;
const [copyHintVisible, setCopyHintVisible] = React.useState(false);
const copyHintTimeoutRef = React.useRef<number | null>(null);
@@ -899,11 +897,9 @@ const AssistantMessageBody: React.FC<Omit<MessageBodyProps, 'isUser'>> = ({
}}
onTouchStart={isTouchContext && canCopyMessage && hasCopyableText ? revealCopyHint : undefined}
>
<div
className="px-3"
>
<div>
<div
className="leading-normal overflow-hidden text-foreground/90 [&_p:last-child]:mb-0 [&_ul:last-child]:mb-0 [&_ol:last-child]:mb-0"
className="leading-relaxed overflow-hidden text-foreground/90 [&_p:last-child]:mb-0 [&_ul:last-child]:mb-0 [&_ol:last-child]:mb-0"
>
{renderedParts}
{showErrorMessage && (
@@ -19,7 +19,7 @@ const MessageHeader: React.FC<MessageHeaderProps> = ({ isUser, providerID, agent
return (
<FadeInOnReveal>
<div className={cn('pl-3', 'mb-2')}>
<div className={cn('mb-2')}>
<div className={cn('flex items-center justify-between gap-2')}>
<div className="flex items-center gap-2">
<div className="flex-shrink-0">