seq forces the term into weak-head normal form. Basically just enough to tell whether the list is `` or not. It's the same kind of evaluation that would happen if you pattern matched on the constructor. Only one Char needs to be read in order to make that determination. Sometimes this is desirable, but other times you want to traverse the whole structure and make sure everything is forced. The confusion is made much worse because you're using lazy IO.
The `takeWhile (/= '\04')` is redundant here, the ^D never ends up in that string.
Most cases where you use `seq` rather than `deepseq` are because you know that the substructure is already evaluated, or there is no need to evaluate it, and it may be very inefficient to do that redundant work.