I found something that seems to work OK upon initial testing: essentially rebind <tab> to pcomplete-std-complete , but since that function isn't interactive for some reason, you need to wrap it:

(define-key eshell-mode-map (kbd "<tab>") (lambda () (interactive) (pcomplete-std-complete)))

The pcompete-std-complete tries to use the completions written for pcomplete with the standard completion UI, which ivy by default will override (with the very recent update, they appear in a nice overlay right in the eshell buffer near point!).

The comments in the source code of pcomplete-completions-at-point (which pcomplete-std-complete depends on) mention a couple of potential problems:

;; FIXME: it only completes the text before point, whereas the ;; standard UI may also consider text after point. ;; FIXME: the `pcomplete' UI may be used internally during ;; pcomplete-completions and then throw to `pcompleted', thus ;; imposing the pcomplete UI over the standard UI.

I think the first one won't affect me, since I don't think I ever press <tab> except at the end of the input line. I haven't seen the second problem surface either, so far.

EDIT: For those unfamiliar with eshell's quirks (this is a euphemism), maybe I should add that to rebind eshell's key as suggested above you should put this in your init file:

(add-hook 'eshell-mode-hook (lambda () (define-key eshell-mode-map (kbd "<tab>") (lambda () (interactive) (pcomplete-std-complete)))))