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:
committed by
GitHub
parent
39e625d8ec
commit
844562749d
@@ -632,6 +632,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
const sessions = useSessionStore((state) => state.sessions);
|
||||
const sessionsByDirectory = useSessionStore((state) => state.sessionsByDirectory);
|
||||
const currentSessionId = useSessionStore((state) => state.currentSessionId);
|
||||
const newSessionDraftOpen = useSessionStore((state) => Boolean(state.newSessionDraft?.open));
|
||||
const setCurrentSession = useSessionStore((state) => state.setCurrentSession);
|
||||
const updateSessionTitle = useSessionStore((state) => state.updateSessionTitle);
|
||||
const shareSession = useSessionStore((state) => state.shareSession);
|
||||
@@ -882,8 +883,11 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
setSessionSwitcherOpen(false);
|
||||
}
|
||||
|
||||
if (!allowReselect && sessionId === currentSessionId) {
|
||||
onSessionSelected?.(sessionId);
|
||||
// Always return early if same session is selected to avoid unnecessary store operations
|
||||
if (sessionId === currentSessionId) {
|
||||
if (!allowReselect) {
|
||||
onSessionSelected?.(sessionId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
setCurrentSession(sessionId);
|
||||
@@ -1458,6 +1462,11 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
|
||||
const previousActiveProjectRef = React.useRef<string | null>(null);
|
||||
React.useLayoutEffect(() => {
|
||||
// While a new session draft is open, keep the sidebar from auto-selecting remembered/fallback sessions.
|
||||
// This is especially important in VS Code where the sidebar view is frequently mounted/unmounted.
|
||||
if (newSessionDraftOpen) {
|
||||
return;
|
||||
}
|
||||
if (!activeProjectId || previousActiveProjectRef.current === activeProjectId) {
|
||||
return;
|
||||
}
|
||||
@@ -1467,6 +1476,21 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
}
|
||||
previousActiveProjectRef.current = activeProjectId;
|
||||
const projectMap = projectSessionMeta.metaByProject.get(activeProjectId);
|
||||
|
||||
// If we already have an active session that belongs to this project (eg user just selected it,
|
||||
// or sidebar remounted after "back"), do NOT override it with remembered/fallback session.
|
||||
if (currentSessionId && projectMap && projectMap.has(currentSessionId)) {
|
||||
setActiveSessionByProject((prev) => {
|
||||
if (prev.get(activeProjectId) === currentSessionId) {
|
||||
return prev;
|
||||
}
|
||||
const next = new Map(prev);
|
||||
next.set(activeProjectId, currentSessionId);
|
||||
return next;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!projectMap || projectMap.size === 0) {
|
||||
setActiveMainTab('chat');
|
||||
if (mobileVariant) {
|
||||
@@ -1492,6 +1516,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
activeSessionByProject,
|
||||
currentSessionId,
|
||||
handleSessionSelect,
|
||||
newSessionDraftOpen,
|
||||
mobileVariant,
|
||||
openNewSessionDraft,
|
||||
projectSections,
|
||||
@@ -1967,7 +1992,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
);
|
||||
|
||||
const renderGroupSessions = React.useCallback(
|
||||
(group: SessionGroup, groupKey: string, projectId?: string | null) => {
|
||||
(group: SessionGroup, groupKey: string, projectId?: string | null, hideGroupLabel?: boolean) => {
|
||||
const isExpanded = expandedSessionGroups.has(groupKey);
|
||||
const isCollapsed = collapsedGroups.has(groupKey);
|
||||
const maxVisible = hideDirectoryControls ? 10 : 5;
|
||||
@@ -1997,11 +2022,49 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
&& normalizedGroupDirectory === currentSessionDirectory,
|
||||
);
|
||||
|
||||
// VS Code sessions list uses a separate header (Agent Manager / New Session).
|
||||
// When the caller requests a flat list (hideGroupLabel), omit the per-group header entirely.
|
||||
if (hideGroupLabel) {
|
||||
return (
|
||||
<div className="oc-group">
|
||||
<div className="oc-group-body pb-3">
|
||||
{visibleSessions.map((node) => renderSessionNode(node, 0, group.directory, projectId))}
|
||||
{totalSessions === 0 ? (
|
||||
<div className="py-1 text-left typography-micro text-muted-foreground">
|
||||
No sessions in this workspace yet.
|
||||
</div>
|
||||
) : null}
|
||||
{remainingCount > 0 && !isExpanded ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleGroupSessionLimit(groupKey)}
|
||||
className="mt-0.5 flex items-center justify-start rounded-md px-1.5 py-0.5 text-left text-xs text-muted-foreground/70 leading-tight hover:text-foreground hover:underline"
|
||||
>
|
||||
Show {remainingCount} more {remainingCount === 1 ? 'session' : 'sessions'}
|
||||
</button>
|
||||
) : null}
|
||||
{isExpanded && totalSessions > maxVisible ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleGroupSessionLimit(groupKey)}
|
||||
className="mt-0.5 flex items-center justify-start rounded-md px-1.5 py-0.5 text-left text-xs text-muted-foreground/70 leading-tight hover:text-foreground hover:underline"
|
||||
>
|
||||
Show fewer sessions
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="oc-group">
|
||||
<div
|
||||
className="group/gh flex items-center justify-between gap-2 py-1 min-w-0 rounded-sm hover:bg-interactive-hover/50 cursor-pointer"
|
||||
onClick={() => {
|
||||
className={cn(
|
||||
"group/gh flex items-center justify-between gap-2 py-1 min-w-0 rounded-sm",
|
||||
!hideGroupLabel && "hover:bg-interactive-hover/50 cursor-pointer"
|
||||
)}
|
||||
onClick={!hideGroupLabel ? () => {
|
||||
setCollapsedGroups((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(groupKey)) {
|
||||
@@ -2011,10 +2074,10 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(event) => {
|
||||
} : undefined}
|
||||
role={!hideGroupLabel ? "button" : undefined}
|
||||
tabIndex={!hideGroupLabel ? 0 : undefined}
|
||||
onKeyDown={!hideGroupLabel ? (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
setCollapsedGroups((prev) => {
|
||||
@@ -2027,29 +2090,31 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}}
|
||||
aria-label={isCollapsed ? `Expand ${group.label}` : `Collapse ${group.label}`}
|
||||
} : undefined}
|
||||
aria-label={!hideGroupLabel ? (isCollapsed ? `Expand ${group.label}` : `Collapse ${group.label}`) : undefined}
|
||||
>
|
||||
<div className="min-w-0 flex items-center gap-1.5 px-0">
|
||||
{isCollapsed ? (
|
||||
<RiArrowRightSLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
|
||||
) : (
|
||||
<RiArrowDownSLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
{!group.isMain || isGitProject ? (
|
||||
<RiGitBranchLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
|
||||
) : null}
|
||||
<div className="min-w-0 flex flex-col justify-center">
|
||||
<p className={cn('text-[15px] font-semibold truncate', isActiveGroup ? 'text-primary' : 'text-muted-foreground')}>
|
||||
{group.label}
|
||||
</p>
|
||||
{showBranchSubtitle ? (
|
||||
<span className="text-[10px] sm:text-[11px] text-muted-foreground/80 truncate leading-tight">
|
||||
{group.branch}
|
||||
</span>
|
||||
{!hideGroupLabel ? (
|
||||
<div className="min-w-0 flex items-center gap-1.5 px-0">
|
||||
{isCollapsed ? (
|
||||
<RiArrowRightSLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
|
||||
) : (
|
||||
<RiArrowDownSLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
|
||||
)}
|
||||
{!group.isMain || isGitProject ? (
|
||||
<RiGitBranchLine className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />
|
||||
) : null}
|
||||
<div className="min-w-0 flex flex-col justify-center">
|
||||
<p className={cn('text-[15px] font-semibold truncate', isActiveGroup ? 'text-primary' : 'text-muted-foreground')}>
|
||||
{group.label}
|
||||
</p>
|
||||
{showBranchSubtitle ? (
|
||||
<span className="text-[10px] sm:text-[11px] text-muted-foreground/80 truncate leading-tight">
|
||||
{group.branch}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : <div />}
|
||||
{group.directory ? (
|
||||
<div className="flex items-center gap-1 px-0.5">
|
||||
{!group.isMain && group.worktree ? (
|
||||
@@ -2404,7 +2469,8 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
||||
);
|
||||
}
|
||||
const groupKey = `${activeSection.project.id}:${group.id}`;
|
||||
return renderGroupSessions(group, groupKey, activeSection.project.id);
|
||||
// In VS Code mode with showOnlyMainWorkspace, hide the group header to show a flat session list
|
||||
return renderGroupSessions(group, groupKey, activeSection.project.id, showOnlyMainWorkspace);
|
||||
})()}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user