Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions apps/web/src/components/cloud-agent-next/ChatSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
Trash2,
X,
Pencil,
LoaderCircle,
} from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { TimeAgo } from '@/components/shared/TimeAgo';
Expand Down Expand Up @@ -107,6 +108,7 @@
currentSessionId?: string;
organizationId?: string;
onDeleteSession?: (sessionId: string) => void;
deletingSessionId?: string;
onRenameSession?: (sessionId: string, title: string) => Promise<void>;
isInSheet?: boolean;
activeSessions?: ActiveSession[];
Expand All @@ -125,6 +127,7 @@
isActive,
isLive,
onDeleteSession,
isDeleting,
onStartRename,
isEditing,
editTitle,
Expand All @@ -137,6 +140,7 @@
isActive: boolean;
isLive: boolean;
onDeleteSession?: (sessionId: string) => void;
isDeleting: boolean;
onStartRename?: () => void;
isEditing: boolean;
editTitle: string;
Expand Down Expand Up @@ -179,11 +183,12 @@

return (
<div
onClick={isEditing ? undefined : onClick}
onClick={isEditing || isDeleting ? undefined : onClick}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className={cn(
'hover:bg-accent cursor-pointer rounded-lg text-sm transition-colors',
isDeleting && 'cursor-wait opacity-60',
isActive && 'bg-accent font-medium'
)}
>
Expand All @@ -202,7 +207,12 @@
<span className="line-clamp-1 min-w-0 flex-1 leading-snug">{session.prompt}</span>
<SessionPrIndicator session={session} />
<span className="relative flex w-6 shrink-0 justify-end">
{shouldReplaceTime ? (
{isDeleting ? (
<LoaderCircle
className="text-muted-foreground h-4 w-4 animate-spin"
aria-label="Deleting session"
/>
) : shouldReplaceTime ? (
<span
className={cn(
'flex h-4 w-4 items-center justify-center',
Expand All @@ -226,7 +236,7 @@
<TimeAgo timestamp={session.updatedAt} compact />
</span>
)}
{(onDeleteSession || onStartRename) && (
{!isDeleting && (onDeleteSession || onStartRename) && (
<span
className={cn(
'absolute inset-y-0 right-0 flex items-center',
Expand Down Expand Up @@ -257,7 +267,10 @@
{onDeleteSession && (
<DropdownMenuItem
variant="destructive"
onClick={() => onDeleteSession(session.sessionId)}
onClick={e => {
e.stopPropagation();
onDeleteSession(session.sessionId);
}}
>
<Trash2 className="h-4 w-4" />
Delete session
Expand Down Expand Up @@ -594,6 +607,7 @@
isLive={activeSessionIds.has(session.sessionId)}
onDeleteSession={onDeleteSession}
onStartRename={onRenameSession ? () => handleStartRename(session) : undefined}
isDeleting={deletingSessionId === session.sessionId}

Check failure on line 610 in apps/web/src/components/cloud-agent-next/ChatSidebar.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Cannot find name 'deletingSessionId'. Did you mean 'editingSessionId'?
isEditing={editingSessionId === session.sessionId}
editTitle={editTitle}
onEditTitleChange={setEditTitle}
Expand Down
80 changes: 66 additions & 14 deletions apps/web/src/components/cloud-agent-next/CloudSidebarLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ import { useSidebarSessions } from './hooks/useSidebarSessions';
import { useActiveSessions } from './hooks/useActiveSessions';
import { deleteSessionFromStoreAtom } from './store/db-session-atoms';
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { useLocalStorage } from '@/hooks/useLocalStorage';

// Context for children to toggle the mobile sidebar sheet
Expand Down Expand Up @@ -49,6 +59,8 @@ export function CloudSidebarLayout({ organizationId, children }: CloudSidebarLay
initializeWithValue: false,
});
const [mobileSheetOpen, setMobileSheetOpen] = useState(false);
const [deletingSessionId, setDeletingSessionId] = useState<string>();
const [sessionPendingDeletion, setSessionPendingDeletion] = useState<string>();
const repoUpdatedSince = useMemo(() => startOfDay(subDays(new Date(), 30)).toISOString(), []);

const createdOnPlatform = useMemo(() => {
Expand Down Expand Up @@ -107,30 +119,32 @@ export function CloudSidebarLayout({ organizationId, children }: CloudSidebarLay

const handleDeleteSession = useCallback(
async (sessionId: string) => {
// Navigate away if deleting the current session
if (sessionId === currentSessionId) {
const basePath = organizationId ? `/organizations/${organizationId}/cloud` : '/cloud';
router.push(basePath);
setDeletingSessionId(sessionId);
try {
await deleteCliSessionV2({ session_id: sessionId });
} catch (error) {
console.error('Error calling session deletion API:', error);
toast.error('Failed to delete session');
return false;
} finally {
setDeletingSessionId(undefined);
}

// Delete from IndexedDB (optimistic)
try {
await deleteSessionFromStore(sessionId);
} catch (error) {
console.error('Error deleting session from IndexedDB:', error);
}

// Delete from server
try {
await deleteCliSessionV2({ session_id: sessionId });
toast('Session deleted successfully');
} catch (error) {
console.error('Error calling session deletion API:', error);
toast.error('Failed to delete session');
if (sessionId === currentSessionId) {
const basePath = organizationId ? `/organizations/${organizationId}/cloud` : '/cloud';
router.push(basePath);
}

toast('Session deleted successfully');
void queryClient.invalidateQueries(trpc.cliSessionsV2.list.pathFilter());
refetchSessions();
return true;
},
[
currentSessionId,
Expand All @@ -144,6 +158,15 @@ export function CloudSidebarLayout({ organizationId, children }: CloudSidebarLay
]
);

const handleConfirmDelete = useCallback(async () => {
if (!sessionPendingDeletion) return;

const wasDeleted = await handleDeleteSession(sessionPendingDeletion);
if (wasDeleted) {
setSessionPendingDeletion(undefined);
}
}, [handleDeleteSession, sessionPendingDeletion]);

const handleRenameSession = useCallback(
async (sessionId: string, title: string) => {
await renameCliSessionV2({ session_id: sessionId, title });
Expand All @@ -170,7 +193,8 @@ export function CloudSidebarLayout({ organizationId, children }: CloudSidebarLay
sessions={sessions}
currentSessionId={currentSessionId}
organizationId={organizationId}
onDeleteSession={handleDeleteSession}
onDeleteSession={setSessionPendingDeletion}
deletingSessionId={deletingSessionId}
onRenameSession={handleRenameSession}
isInSheet
activeSessions={activeSessions}
Expand All @@ -192,7 +216,8 @@ export function CloudSidebarLayout({ organizationId, children }: CloudSidebarLay
sessions={sessions}
currentSessionId={currentSessionId}
organizationId={organizationId}
onDeleteSession={handleDeleteSession}
onDeleteSession={setSessionPendingDeletion}
deletingSessionId={deletingSessionId}
onRenameSession={handleRenameSession}
activeSessions={activeSessions}
searchQuery={searchQuery}
Expand All @@ -208,6 +233,33 @@ export function CloudSidebarLayout({ organizationId, children }: CloudSidebarLay
{/* Main Content */}
<div className="h-full flex-1 overflow-hidden">{children}</div>
</div>
<AlertDialog
open={sessionPendingDeletion !== undefined}
onOpenChange={open => {
if (!open && !deletingSessionId) {
setSessionPendingDeletion(undefined);
}
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete session?</AlertDialogTitle>
<AlertDialogDescription>
This permanently deletes the session and its history.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={deletingSessionId !== undefined}>Cancel</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
disabled={deletingSessionId !== undefined}
onClick={handleConfirmDelete}
>
{deletingSessionId ? 'Deleting...' : 'Delete session'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</SidebarLayoutContext.Provider>
);
}
Loading