feat: add session management (rename, archive, delete)

Implements comprehensive session lifecycle management with user-friendly interactions:

Rename: Update session metadata.name via PATCH endpoint with conflict detection
Archive: Abort active sessions via DELETE endpoint with validation
Delete: Permanently remove inactive sessions with cascade cleanup

Backend:
- Store.deleteSession() removes session and cascade-deletes messages
- SyncEngine.renameSession() with concurrency error handling
- SyncEngine.deleteSession() with active session validation
- PATCH /sessions/:id for rename, DELETE /sessions/:id for delete

Frontend Components:
- RenameSessionDialog: Text input with auto-focus and error display
- SessionActionMenu: Modal with rename, archive, delete buttons
- ConfirmDialog: Reusable confirmation with error feedback
- SessionHeader: Menu button (⋮) triggering action menu
- SessionList: Long-press detection triggering item actions

Interactions:
- Long-press on session list items (500ms threshold) opens action menu
- Menu button in session header (non-Telegram environments only)
- Confirmation dialogs with descriptive warnings for destructive actions
- Real-time error display in dialogs on operation failure
- Haptic feedback on long-press via usePlatform hook

Accessibility:
- Keyboard support (Enter/Space) for long-press handler
- Focus management in RenameSessionDialog
- Proper ARIA labels and semantic HTML
This commit is contained in:
weishu
2026-01-02 19:15:01 +08:00
parent c2d1f8c507
commit 41fd1abd53
13 changed files with 881 additions and 98 deletions
+30 -1
View File
@@ -25,6 +25,8 @@ export function useSessionActions(api: ApiClient | null, sessionId: string | nul
switchSession: () => Promise<void>
setPermissionMode: (mode: PermissionMode) => Promise<void>
setModelMode: (mode: ModelMode) => Promise<void>
renameSession: (name: string) => Promise<void>
deleteSession: () => Promise<void>
isPending: boolean
} {
const queryClient = useQueryClient()
@@ -75,11 +77,38 @@ export function useSessionActions(api: ApiClient | null, sessionId: string | nul
onSuccess: () => void invalidateSession(),
})
const renameMutation = useMutation({
mutationFn: async (name: string) => {
if (!api || !sessionId) {
throw new Error('Session unavailable')
}
await api.renameSession(sessionId, name)
},
onSuccess: () => void invalidateSession(),
})
const deleteMutation = useMutation({
mutationFn: async () => {
if (!api || !sessionId) {
throw new Error('Session unavailable')
}
await api.deleteSession(sessionId)
},
onSuccess: async () => {
if (!sessionId) return
queryClient.removeQueries({ queryKey: queryKeys.session(sessionId) })
queryClient.removeQueries({ queryKey: queryKeys.messages(sessionId) })
await queryClient.invalidateQueries({ queryKey: queryKeys.sessions })
},
})
return {
abortSession: abortMutation.mutateAsync,
switchSession: switchMutation.mutateAsync,
setPermissionMode: permissionMutation.mutateAsync,
setModelMode: modelMutation.mutateAsync,
isPending: abortMutation.isPending || switchMutation.isPending || permissionMutation.isPending || modelMutation.isPending,
renameSession: renameMutation.mutateAsync,
deleteSession: deleteMutation.mutateAsync,
isPending: abortMutation.isPending || switchMutation.isPending || permissionMutation.isPending || modelMutation.isPending || renameMutation.isPending || deleteMutation.isPending,
}
}