Every AI product has, at some point, made a quiet choice about what the user sees while a model is thinking.

Most pick the spinner. The pattern is: button click → a translucent overlay → some loading copy that tries to be charming → the finished artifact appears all at once when generation finishes. Easy to ship. Almost always the wrong call.

Strive generates entire courses (modules, lessons, code blocks, callouts, quizzes) using LangGraph agents on Anthropic Claude. A lesson is not a one-shot text completion. It's a tree of generation that can take real wall-clock time. We had a fork in the road early on: spinner-and-wait, or stream-as-it-generates. We picked streaming. This post is about why, what it actually took to build, and the parts we got wrong on the first pass.

The default is a spinner because the default is easier

Spinner UX has one virtue: it lets the backend be a black box. The endpoint returns when it returns. The frontend renders a single payload. There is one happy path and one error path. Errors are clean. You either got the artifact or you didn't.