feat: archive sessions from the mobile session row

Add a two-step archive affordance to each mobile session row, mirroring the
project remove confirmation: tapping the archive icon arms an inline confirm
(with cancel), and confirming archives the session. Only one row can be in
the confirming state at a time.

Also relabel the project remove confirmation from "Delete" to "Close"
across all locales, since removing a project closes it from the app rather
than deleting anything.
This commit is contained in:
Bohdan Triapitsyn
2026-06-02 16:20:12 +03:00
parent 79d68ba583
commit 7065bcb2ba
9 changed files with 95 additions and 11 deletions
+71 -3
View File
@@ -1,6 +1,7 @@
import React from 'react';
import {
RiAddLine,
RiArchiveLine,
RiArrowDownSLine,
RiArrowUpSLine,
RiCheckLine,
@@ -245,8 +246,22 @@ const SessionRow: React.FC<{
indent: number;
/** When provided, shown as a small second-line subtitle below the title (e.g. "Project · branch"). */
contextLabel?: string;
/** When true, the row shows the two-step archive confirmation. */
confirmingArchive?: boolean;
onSelect: () => void;
}> = ({ session, active, indent, contextLabel, onSelect }) => {
/** When provided, an archive affordance is shown; first tap arms confirm, X cancels. */
onRequestArchive?: () => void;
onConfirmArchive?: () => void;
}> = ({
session,
active,
indent,
contextLabel,
confirmingArchive = false,
onSelect,
onRequestArchive,
onConfirmArchive,
}) => {
const { t } = useI18n();
const time = formatRelativeShort(getSessionTimestamp(session));
const title = session.title?.trim() || t('mobile.sessions.untitled');
@@ -254,14 +269,19 @@ const SessionRow: React.FC<{
<div
className={cn(
'flex items-center gap-1 transition-colors',
active && 'bg-[color-mix(in_srgb,var(--primary)_10%,transparent)]',
active && !confirmingArchive && 'bg-[color-mix(in_srgb,var(--primary)_10%,transparent)]',
confirmingArchive && 'bg-[color-mix(in_srgb,var(--destructive)_8%,transparent)]',
)}
>
<button
type="button"
className="flex min-h-12 min-w-0 flex-1 items-center gap-2.5 py-2 pr-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset"
className={cn(
'flex min-h-12 min-w-0 flex-1 items-center gap-2.5 py-2 pr-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset',
confirmingArchive && 'opacity-50',
)}
style={{ paddingLeft: indent, touchAction: 'manipulation' }}
onClick={onSelect}
disabled={confirmingArchive}
>
<span className="flex min-w-0 flex-1 flex-col">
<span className="flex items-center gap-2.5">
@@ -289,6 +309,35 @@ const SessionRow: React.FC<{
) : null}
</span>
</button>
{onRequestArchive ? (
<>
{confirmingArchive ? (
<button
type="button"
className="flex h-9 shrink-0 items-center gap-1.5 rounded-xl bg-destructive px-3 text-destructive-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive"
aria-label={t('mobile.sessions.archiveSessionAria', { title })}
onClick={onConfirmArchive}
style={{ touchAction: 'manipulation' }}
>
<RiArchiveLine className="size-4" />
<span className="typography-ui-label">{t('sessions.sidebar.bulkActions.archive')}</span>
</button>
) : null}
<button
type="button"
className="mr-1.5 flex size-9 shrink-0 items-center justify-center rounded-xl text-muted-foreground/70 transition-colors hover:bg-interactive-hover hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
aria-label={
confirmingArchive
? t('mobile.sessions.cancelArchiveAria', { title })
: t('mobile.sessions.archiveSessionAria', { title })
}
onClick={onRequestArchive}
style={{ touchAction: 'manipulation' }}
>
{confirmingArchive ? <RiCloseLine className="size-4" /> : <RiArchiveLine className="size-4" />}
</button>
</>
) : null}
</div>
);
};
@@ -429,6 +478,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession);
const archiveSession = useSessionUIStore((state) => state.archiveSession);
const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft);
const setActiveProject = useProjectsStore((state) => state.setActiveProject);
const setActiveProjectIdOnly = useProjectsStore((state) => state.setActiveProjectIdOnly);
@@ -441,6 +491,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
const worktreeOrderByProject = useWorktreeOrderStore((state) => state.orderByProject);
const [query, setQuery] = React.useState('');
const [editingProjectId, setEditingProjectId] = React.useState<string | null>(null);
const [confirmingArchiveSessionId, setConfirmingArchiveSessionId] = React.useState<string | null>(null);
// Bumped to force a re-list of worktrees (e.g. after one is deleted in the editor).
const [worktreeRefreshKey, setWorktreeRefreshKey] = React.useState(0);
const [directoryDialogOpen, setDirectoryDialogOpen] = React.useState(false);
@@ -463,6 +514,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
setConfirmingDeleteId(null);
setVisibleCountByBucket(new Map());
setEditingProjectId(null);
setConfirmingArchiveSessionId(null);
return;
}
void refreshGlobalSessions(liveSessions);
@@ -663,7 +715,10 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
session={session}
active={currentSessionId === session.id}
indent={indent}
confirmingArchive={confirmingArchiveSessionId === session.id}
onSelect={() => handleSelectSession(session)}
onRequestArchive={() => handleRequestArchive(session.id)}
onConfirmArchive={() => void handleConfirmArchive(session)}
/>
))}
{remaining > 0 ? (
@@ -702,6 +757,19 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
onOpenChange(false);
};
// Two-step archive: first tap arms the confirm on that row, second confirms.
// Only one row can be in the confirming state at a time.
const handleRequestArchive = (sessionId: string) => {
setConfirmingArchiveSessionId((current) => (current === sessionId ? null : sessionId));
};
const handleConfirmArchive = async (session: Session) => {
setConfirmingArchiveSessionId(null);
const ok = await archiveSession(session.id);
if (ok) toast.success(t('sessions.sidebar.session.archive.success'));
else toast.error(t('sessions.sidebar.session.archive.error'));
};
const handleStartNewChat = () => {
openNewSessionDraft();
onOpenChange(false);