Skip to content
Merged
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
9 changes: 9 additions & 0 deletions apps/docs/fast-sessions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,15 @@ reversible and hides the session and its linked tasks from ordinary lists and
search until it is unarchived. Deleting a session uses the existing confirmation
and cleanup safeguards.

In the Sessions list or board, choose **Select sessions** to show checkboxes.
Select sessions individually, or choose **All** to select every session you can
manage on the current page. **Delete (n)** confirms and deletes the selection
using the same cleanup safeguards as individual deletion. If some deletions
fail, those sessions stay selected for retry. Exit selection mode to resume
opening sessions normally; changing views, filters, or pages clears selection.
Deleting one or several sessions from either overview keeps you on that view
with its current filters and page.

On desktop, you can keep several task workspaces open beside the session,
resize them, and switch between the conversation and task panels without losing
your place. On smaller screens, Roomote shows one workspace at a time so the
Expand Down
19 changes: 11 additions & 8 deletions apps/web/src/app/(authenticated)/sessions/SessionBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {

import { BasicTooltip, Button, Funnel } from '@/components/system';
import { useSessionStatusMutation } from '@/components/sessions/use-session-status-mutation';
import { useSessionSelectionActive } from '@/components/sessions/SessionSelection';
import {
canDropSessionBoardCard,
getSessionBoardDropStatus,
Expand Down Expand Up @@ -454,6 +455,8 @@ export function SessionBoardCard({
}) {
const reducedMotion = useReducedMotion() ?? false;
const registry = useContext(SessionBoardCardRegistryContext);
const selecting = useSessionSelectionActive();
const canDrag = canManage && !selecting;
const dnd = useContext(SessionBoardDndContext);
const [isFlying, setIsFlying] = useState(false);
const dragData = useRef<SessionBoardDragData>({
Expand All @@ -470,7 +473,7 @@ export function SessionBoardCard({
const draggable = useDraggable({
id: sessionId,
data: dragData.current,
disabled: !canManage || dnd?.statusMutationPending === true,
disabled: !canDrag || dnd?.statusMutationPending === true,
});

useLayoutEffect(() => {
Expand Down Expand Up @@ -529,14 +532,14 @@ export function SessionBoardCard({
<div
ref={setDragNodeRef}
style={{ transform: dragTransform }}
className={`relative w-full ${canManage ? 'cursor-grab active:cursor-grabbing' : ''}`}
{...(canManage ? draggable.attributes : {})}
{...(canManage ? dragListeners : {})}
role={canManage ? 'group' : undefined}
aria-label={canManage ? `Move ${title} to another status` : undefined}
className={`relative w-full ${canDrag ? 'cursor-grab active:cursor-grabbing' : ''}`}
{...(canDrag ? draggable.attributes : {})}
{...(canDrag ? dragListeners : {})}
role={canDrag ? 'group' : undefined}
aria-label={canDrag ? `Move ${title} to another status` : undefined}
aria-pressed={undefined}
title={canManage ? 'Drag to change session status' : undefined}
data-session-board-drag-activator={canManage ? true : undefined}
title={canDrag ? 'Drag to change session status' : undefined}
data-session-board-drag-activator={canDrag ? true : undefined}
>
{children}
</div>
Expand Down
78 changes: 47 additions & 31 deletions apps/web/src/app/(authenticated)/sessions/SessionCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Link from 'next/link';
import { format, formatDistanceToNow, isSameDay, isSameYear } from 'date-fns';
import type { SessionManualStatus } from '@roomote/types';

Expand All @@ -21,6 +20,11 @@ import { SessionInferenceCostBreakdown } from '@/components/sessions/SessionInfe
import { PrivateSessionIcon } from '@/components/sessions/PrivateSessionIcon';
import { getSessionSurfaceLabel } from '@/components/sessions/session-surfaces';
import { TaskAutomationIcon } from '@/components/tasks/TaskAutomationIcon';
import {
SessionSelectionControl,
SessionSelectionLink,
SessionSelectionRow,
} from '@/components/sessions/SessionSelection';
import { SessionActions } from '../../(sandbox)/sessions/[sessionId]/SessionDeleteAction';

type SessionCardData = {
Expand Down Expand Up @@ -108,41 +112,53 @@ export function SessionCard({
const accessibleActivityTimestamp = activityDate.toLocaleString();

return (
<div className="ph-no-capture group/card relative flex w-full @container items-start gap-3 p-4 transition-colors hover:bg-accent-foreground/10 group-data-[dragging=true]/board:hover:bg-transparent">
<Link
<SessionSelectionRow
sessionId={session.id}
className="ph-no-capture group/card relative flex w-full @container items-start gap-3 p-4 transition-colors hover:bg-accent-foreground/10 group-data-[dragging=true]/board:hover:bg-transparent"
>
<SessionSelectionLink
sessionId={session.id}
title={session.title}
canManage={session.canManage ?? false}
href={`/sessions/${session.id}`}
className="absolute inset-0 z-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<span className="sr-only">{session.title}</span>
</Link>
</SessionSelectionLink>
<div className="pointer-events-none relative z-10 mt-1 shrink-0">
{session.ownerKind === 'automation' ? (
<span
className="flex size-8 items-center justify-center overflow-clip rounded-full border border-border bg-white dark:bg-muted"
aria-hidden="true"
>
<TaskAutomationIcon
automationKey={session.ownerAutomation}
className="size-7"
<SessionSelectionControl
sessionId={session.id}
title={session.title}
canManage={session.canManage ?? false}
>
{session.ownerKind === 'automation' ? (
<span
className="flex size-8 items-center justify-center overflow-clip rounded-full border border-border bg-white dark:bg-muted"
aria-hidden="true"
>
<TaskAutomationIcon
automationKey={session.ownerAutomation}
className="size-7"
/>
</span>
) : (
<Avatar
imageUrl={session.ownerImageUrl}
name={ownerDisplayName}
email={session.ownerEmail ?? undefined}
size="md"
alt=""
loading="eager"
decoding="sync"
/>
</span>
) : (
<Avatar
imageUrl={session.ownerImageUrl}
name={ownerDisplayName}
email={session.ownerEmail ?? undefined}
size="md"
alt=""
loading="eager"
decoding="sync"
/>
)}
{session.unread && session.ownerUserId === viewerUserId ? (
<span
aria-label="Unread activity"
className="absolute top-0 right-0 size-2 rounded-full bg-accent-foreground ring-2 ring-background"
/>
) : null}
)}
{session.unread && session.ownerUserId === viewerUserId ? (
<span
aria-label="Unread activity"
className="absolute top-0 right-0 size-2 rounded-full bg-accent-foreground ring-2 ring-background"
/>
) : null}
</SessionSelectionControl>
</div>
<div className="pointer-events-none relative z-10 min-w-0 flex-1">
<div className="flex items-start justify-between gap-2 text-xs text-muted-foreground/75 md:items-center">
Expand Down Expand Up @@ -230,6 +246,6 @@ export function SessionCard({
/>
</div>
) : null}
</div>
</SessionSelectionRow>
);
}
Loading
Loading