feat(ui): enable drag-and-drop attachments and image previews in chat (#390)

* feat(BottomTerminalDock): add close button next to the fullscreen toggle in the dock

* style: replace hardcoded gradient with theme value in shine text variant

* fix(header): adapt instance button for desktop only

* refactor(chat): polish sticky turn UX and message action rows for better readability

Switch to stable sticky-only turn behavior and redesign user/assistant action controls (placement, hover rules, ordering, spacing, selection-safe clamp) to reduce visual noise and improve interaction flow.

* feat(chat/message): refactor buttons in messages footer

* feat: enhance image preview functionality in chat messages

- Added a new ImagePreviewDialog component to handle image previews with navigation support.
- Updated ToolOutputDialog to utilize the new ImagePreviewDialog for displaying images.
- Modified the ToolPopupContent type to include a gallery of images and an index for the current image.
- Removed the old inline image display logic from ToolOutputDialog.
- Improved file handling in the file store, including better MIME type guessing and handling of server paths.
- Introduced a new API endpoint for handling large session message payloads, allowing for better management of multi-file attachments.
- Updated the VSCode bridge to support session message requests with appropriate headers and body handling.

* feat(proxy): implement SSE forwarding and enhance generic API request handling

* feat(chat): support submitting only queued messages

* feat: add image preview transition state

* fix: default VSCode view to draft and fixed sessions list regression
This commit is contained in:
Bohdan Triapitsyn
2026-02-11 19:28:22 +02:00
committed by GitHub
parent 39e625d8ec
commit 844562749d
25 changed files with 1621 additions and 489 deletions
@@ -1,5 +1,5 @@
import React from 'react';
import { RiFullscreenExitLine, RiFullscreenLine } from '@remixicon/react';
import { RiCloseLine, RiFullscreenExitLine, RiFullscreenLine } from '@remixicon/react';
import { cn } from '@/lib/utils';
import { useUIStore } from '@/stores/useUIStore';
@@ -157,15 +157,26 @@ export const BottomTerminalDock: React.FC<BottomTerminalDockProps> = ({ isOpen,
)}
{isOpen && (
<button
type="button"
onClick={toggleFullscreen}
className="absolute right-2 top-2 z-30 inline-flex h-8 w-8 items-center justify-center rounded-md text-[var(--surface-muted-foreground)] transition-colors hover:bg-[var(--interactive-hover)] hover:text-[var(--surface-foreground)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
title={isFullscreen ? 'Restore terminal panel height' : 'Expand terminal panel'}
aria-label={isFullscreen ? 'Restore terminal panel height' : 'Expand terminal panel'}
>
{isFullscreen ? <RiFullscreenExitLine className="h-5 w-5" /> : <RiFullscreenLine className="h-5 w-5" />}
</button>
<div className="absolute right-2 top-2 z-30 inline-flex items-center gap-1">
<button
type="button"
onClick={toggleFullscreen}
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-[var(--surface-muted-foreground)] transition-colors hover:bg-[var(--interactive-hover)] hover:text-[var(--surface-foreground)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
title={isFullscreen ? 'Restore terminal panel height' : 'Expand terminal panel'}
aria-label={isFullscreen ? 'Restore terminal panel height' : 'Expand terminal panel'}
>
{isFullscreen ? <RiFullscreenExitLine className="h-5 w-5" /> : <RiFullscreenLine className="h-5 w-5" />}
</button>
<button
type="button"
onClick={() => setBottomTerminalOpen(false)}
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-[var(--surface-muted-foreground)] transition-colors hover:bg-[var(--interactive-hover)] hover:text-[var(--surface-foreground)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
title="Close terminal panel"
aria-label="Close terminal panel"
>
<RiCloseLine className="h-6 w-6" />
</button>
</div>
)}
<div
+8 -4
View File
@@ -882,19 +882,23 @@ export const Header: React.FC = () => {
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label={`Open instance, usage and MCP (current: ${currentInstanceLabel})`}
aria-label={isDesktopApp
? `Open instance, usage and MCP (current: ${currentInstanceLabel})`
: 'Open services, usage and MCP'}
className={cn(
headerIconButtonClass,
'w-auto max-w-[14rem] justify-start gap-1.5 px-2.5'
isDesktopApp
? 'w-auto max-w-[14rem] justify-start gap-1.5 px-2.5'
: 'h-9 w-9'
)}
>
<RiStackLine className="h-5 w-5" />
<span className="truncate text-base font-normal">{currentInstanceLabel}</span>
{isDesktopApp && <span className="truncate text-base font-normal">{currentInstanceLabel}</span>}
</button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>
<p>Current instance: {currentInstanceLabel}</p>
<p>{isDesktopApp ? `Current instance: ${currentInstanceLabel}` : 'Services'}</p>
</TooltipContent>
</Tooltip>
<DropdownMenuContent
@@ -69,7 +69,15 @@ export const VSCodeLayout: React.FC = () => {
const hasAppliedInitialSession = React.useRef(false);
const [currentView, setCurrentView] = React.useState<VSCodeView>('sessions');
const bootDraftOpen = React.useMemo(() => {
try {
return Boolean(useSessionStore.getState().newSessionDraft?.open);
} catch {
return false;
}
}, []);
const [currentView, setCurrentView] = React.useState<VSCodeView>(() => (bootDraftOpen ? 'chat' : 'sessions'));
const [containerWidth, setContainerWidth] = React.useState<number>(0);
const containerRef = React.useRef<HTMLDivElement>(null);
const currentSessionId = useSessionStore((state) => state.currentSessionId);