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);
+3 -1
View File
@@ -64,9 +64,11 @@ export const dict = {
'mobile.sessions.moveDownAria': 'Move {label} down',
'mobile.sessions.removeProjectAria': 'Remove {label}',
'mobile.sessions.cancelRemoveProjectAria': 'Cancel removing {label}',
'mobile.sessions.confirmRemoveProject': 'Delete',
'mobile.sessions.confirmRemoveProject': 'Close',
'mobile.sessions.confirmRemoveProjectAria': 'Confirm removing {label}',
'mobile.sessions.toast.projectRemoved': 'Removed {label}',
'mobile.sessions.archiveSessionAria': 'Archive {title}',
'mobile.sessions.cancelArchiveAria': 'Cancel archiving {title}',
'mobile.projectEdit.worktreesTitle': 'Worktrees',
'mobile.projectEdit.worktreesEmpty': 'No worktrees in this project yet.',
'mobile.projectEdit.reorderHint': 'Drag to reorder worktrees.',
+3 -1
View File
@@ -64,9 +64,11 @@ export const dict: Record<I18nKey, string> = {
"mobile.sessions.moveDownAria": "Mover {label} abajo",
"mobile.sessions.removeProjectAria": "Eliminar {label}",
"mobile.sessions.cancelRemoveProjectAria": "Cancelar eliminación de {label}",
"mobile.sessions.confirmRemoveProject": "Eliminar",
"mobile.sessions.confirmRemoveProject": "Cerrar",
"mobile.sessions.confirmRemoveProjectAria": "Confirmar eliminación de {label}",
"mobile.sessions.toast.projectRemoved": "Se eliminó {label}",
"mobile.sessions.archiveSessionAria": "Archivar {title}",
"mobile.sessions.cancelArchiveAria": "Cancelar archivar {title}",
"mobile.sessions.editProjectAria": "Editar {label}",
"mobile.projectEdit.worktreesTitle": "Worktrees",
"mobile.projectEdit.worktreesEmpty": "Este proyecto aún no tiene worktrees.",
+3 -1
View File
@@ -64,9 +64,11 @@ export const dict: Record<I18nKey, string> = {
'mobile.sessions.moveDownAria': '{label} 아래로 이동',
'mobile.sessions.removeProjectAria': '{label} 제거',
'mobile.sessions.cancelRemoveProjectAria': '{label} 제거 취소',
'mobile.sessions.confirmRemoveProject': '삭제',
'mobile.sessions.confirmRemoveProject': '닫기',
'mobile.sessions.confirmRemoveProjectAria': '{label} 제거 확인',
'mobile.sessions.toast.projectRemoved': '{label} 제거됨',
'mobile.sessions.archiveSessionAria': '{title} 보관',
'mobile.sessions.cancelArchiveAria': '{title} 보관 취소',
'mobile.sessions.editProjectAria': '{label} 편집',
'mobile.projectEdit.worktreesTitle': '워크트리',
'mobile.projectEdit.worktreesEmpty': '이 프로젝트에는 아직 워크트리가 없습니다.',
+3 -1
View File
@@ -65,9 +65,11 @@ export const dict: Record<I18nKey, string> = {
'mobile.sessions.moveDownAria': 'Przenieś {label} w dół',
'mobile.sessions.removeProjectAria': 'Usuń {label}',
'mobile.sessions.cancelRemoveProjectAria': 'Anuluj usuwanie {label}',
'mobile.sessions.confirmRemoveProject': 'Usuń',
'mobile.sessions.confirmRemoveProject': 'Zamknij',
'mobile.sessions.confirmRemoveProjectAria': 'Potwierdź usunięcie {label}',
'mobile.sessions.toast.projectRemoved': 'Usunięto {label}',
'mobile.sessions.archiveSessionAria': 'Zarchiwizuj {title}',
'mobile.sessions.cancelArchiveAria': 'Anuluj archiwizację {title}',
'mobile.sessions.editProjectAria': 'Edytuj {label}',
'mobile.projectEdit.worktreesTitle': 'Worktree',
'mobile.projectEdit.worktreesEmpty': 'Ten projekt nie ma jeszcze worktree.',
+3 -1
View File
@@ -64,9 +64,11 @@ export const dict: Record<I18nKey, string> = {
"mobile.sessions.moveDownAria": "Mover {label} para baixo",
"mobile.sessions.removeProjectAria": "Remover {label}",
"mobile.sessions.cancelRemoveProjectAria": "Cancelar remoção de {label}",
"mobile.sessions.confirmRemoveProject": "Excluir",
"mobile.sessions.confirmRemoveProject": "Fechar",
"mobile.sessions.confirmRemoveProjectAria": "Confirmar remoção de {label}",
"mobile.sessions.toast.projectRemoved": "Removido {label}",
"mobile.sessions.archiveSessionAria": "Arquivar {title}",
"mobile.sessions.cancelArchiveAria": "Cancelar arquivamento de {title}",
"mobile.sessions.editProjectAria": "Editar {label}",
"mobile.projectEdit.worktreesTitle": "Worktrees",
"mobile.projectEdit.worktreesEmpty": "Este projeto ainda não tem worktrees.",
+3 -1
View File
@@ -64,9 +64,11 @@ export const dict: Record<I18nKey, string> = {
"mobile.sessions.moveDownAria": "Перемістити {label} вниз",
"mobile.sessions.removeProjectAria": "Видалити {label}",
"mobile.sessions.cancelRemoveProjectAria": "Скасувати видалення {label}",
"mobile.sessions.confirmRemoveProject": "Видалити",
"mobile.sessions.confirmRemoveProject": "Закрити",
"mobile.sessions.confirmRemoveProjectAria": "Підтвердити видалення {label}",
"mobile.sessions.toast.projectRemoved": "Видалено {label}",
"mobile.sessions.archiveSessionAria": "Архівувати {title}",
"mobile.sessions.cancelArchiveAria": "Скасувати архівування {title}",
"mobile.sessions.editProjectAria": "Редагувати {label}",
"mobile.projectEdit.worktreesTitle": "Ворктрі",
"mobile.projectEdit.worktreesEmpty": "У цьому проєкті ще немає ворктрі.",
+3 -1
View File
@@ -64,9 +64,11 @@ export const dict: Record<I18nKey, string> = {
'mobile.sessions.moveDownAria': '将 {label} 下移',
'mobile.sessions.removeProjectAria': '移除 {label}',
'mobile.sessions.cancelRemoveProjectAria': '取消移除 {label}',
'mobile.sessions.confirmRemoveProject': '删除',
'mobile.sessions.confirmRemoveProject': '关闭',
'mobile.sessions.confirmRemoveProjectAria': '确认移除 {label}',
'mobile.sessions.toast.projectRemoved': '已移除 {label}',
'mobile.sessions.archiveSessionAria': '归档 {title}',
'mobile.sessions.cancelArchiveAria': '取消归档 {title}',
'mobile.sessions.editProjectAria': '编辑 {label}',
'mobile.projectEdit.worktreesTitle': '工作树',
'mobile.projectEdit.worktreesEmpty': '此项目还没有工作树。',
+3 -1
View File
@@ -64,9 +64,11 @@ export const dict: Record<I18nKey, string> = {
'mobile.sessions.moveDownAria': '將 {label} 下移',
'mobile.sessions.removeProjectAria': '移除 {label}',
'mobile.sessions.cancelRemoveProjectAria': '取消移除 {label}',
'mobile.sessions.confirmRemoveProject': '刪除',
'mobile.sessions.confirmRemoveProject': '關閉',
'mobile.sessions.confirmRemoveProjectAria': '確認移除 {label}',
'mobile.sessions.toast.projectRemoved': '已移除 {label}',
'mobile.sessions.archiveSessionAria': '封存 {title}',
'mobile.sessions.cancelArchiveAria': '取消封存 {title}',
'mobile.sessions.editProjectAria': '編輯 {label}',
'mobile.projectEdit.worktreesTitle': '工作樹',
'mobile.projectEdit.worktreesEmpty': '此專案還沒有工作樹。',