ONC2-8555 · app PR to dev, api#1428 to main · updated 28 Oct, 10:55 am IST
The tutor chat, rebuilt
Rezzy, Savvy and Casey now show a reply in the order it happened, a step at a time. The chat stays still while the learner reads.
What learners see
Steps sit where they happened
Before, a reply's thinking steps were pulled above its text, so the answer jumped around as steps arrived. Now each run of steps sits in a "Thinking" group, in order, between the text and cards around it. A step shows the moment it starts. Its detail fills in once it finishes.


The question stays pinned, and done groups fold
Before, the list chased the stream to the bottom, so the learner couldn't read from the top. Now the question moves to the top once and the list stays put. A group folds to "Thought for 4s" once it's done and the answer goes on below it, so a small phone shows the answer, not the steps. A tap opens it again, and the learner's choice sticks. A round "Jump to latest reply" button, 46 pt like the header's buttons, shows while part of the reply is below the screen.



Same chat on Android and iPad
Android folds the same way. Its jump button gets a thin ring, because Android draws the glass as flat white on the white reply. iPad keeps the same layout in landscape.


Smaller things
- Text arrives a paragraph or list item at a time, not word by word with a fading tail. That part is api#1428. Until it reaches staging, staging still sends words.
- "Thinking" and a running step shimmer from a darker grey. They read at 3.8:1 on the chat's blue, up from 2.6:1.
- A stopped reply reads as stopped, with no spinner left running.
- A retried question shows its new reply once, with its own timer.
- The question screen's explanation chat uses the same reply and pin.
- Every course gets the same chat. Only the tutor's name and art change.
- Native pieces where the platform has them. iOS uses the SwiftUI spinner, Android the Compose spinner and shimmer, web our own spinner.
Speed
Measured on the iPhone 18 Pro simulator with a dev build, before the change:
- The composer, header, menu and voice stage each re-rendered about 111 times per reply.
- The hidden past-chats drawer refetched every 1.5 s while a reply ran. Its refetch at the end of a text-heavy reply froze the chat for 3.8 s.
Now each chat has its own store, and every part reads only the slice it needs. A streamed line redraws one reply, not the whole chat. The drawer fetches only while it's open.
Bugs found on the way, now fixed
Android showed nothing while a reply ran
The question sat alone until the whole answer landed. Android's list detaches rows it thinks are off screen, and the native "Thinking" shimmer and spinner stayed at zero size inside a detached row. The chat list now keeps its rows attached.


On a small iPhone the jump button sat on the question
With the keyboard open on an iPhone SE, the button overlapped the pinned question by 5 pt. It now waits while the keyboard is up and comes back when it closes.


The reply went blank as its text finished
When a block of text finished streaming, the app swapped it for a new view so highlighting could turn on. For a frame or a few, the reply was empty: one frame on desktop Chrome, where the card fell to an 11 px strip, and 5 frames (0.17 s) on the iPhone. Now the text keeps its views from first line to last, and highlighting turns on in place. A frame-by-frame check found no drop in 4,178 iPhone frames or 6,388 web frames.
On web the jump button hid while the reply ran past the screen
Up to 700 px of reply sat below the screen with no jump button. The web only measures a view if it was set up to report its size when it first appeared, and the reply's row appeared one step too early. Every message row now reports its size from the start.
A follow-up in the explanation chat ended up four screens down
The explanation chat loads its follow-up suggestions before the learner's earlier turns. A follow-up sent right away pinned, then the earlier turns landed above it and pushed it 2,833 px down. Now the question keeps its place, 22 px from the top in the same test, until the learner scrolls or sends again.
What the design review found, and what happened
A fresh reviewer checked the captures without seeing the code. Each finding was measured in the app before it was fixed or set aside.
| Jump button 40 pt, under the 44 pt tap minimum | Fixed, now 46 pt |
| Android jump button white on white, no edge | Fixed, 1 px ring |
| Thinking group stayed open after the answer, so a small phone showed no answer | Fixed, it folds once done |
| Reply blinked as it finished (web and iPhone) | Fixed, see above |
| "Thinking" label too light on the blue | Fixed, 2.6:1 to 3.8:1 |
| Jump button covers a word of the reply | By design. It floats over the list's fade, as in other chat apps, and hides at the end |
| Step details clamp at two lines | By design. A tap shows the rest, and screen readers hear all of it |
| No copy or like row under the newest reply | By design. The follow-ups take its place. Same on dev |
| Raw markdown flashes on web while text streams | api#1428 fixes it once it reaches staging |
| Step details read like internal notes | The model's own summary. A prompt change on the api side, not this PR |
| Android steps all land at once, and a 4 s tap delay | The cloud device's network and remote taps, not the app |
| Privacy note peeks under the header for a few seconds on web | Same on dev. Reported, not this PR |
Where it was checked
| iPhone 18 Pro | Before and after captures at 3x, a walkthrough video |
| iPhone SE | Text fit, row alignment, keyboard |
| Largest text size | Header stays one line, steps wrap cleanly |
| Android (cloud Pixel 9) | Captures and video after the fixes |
| iPad (cloud) | Portrait and landscape |
| Web, desktop and phone width | Before and after captures, videos, the explanation chat |
| Automated | Typecheck, lint, jest, every CI check, and the end-to-end chat tests (13 of 13 on web) |

For the next engineer
projectReplyturns a message's parts into ordered pieces (thought groups, text, cards), andReplyBodydraws them.ThinkingGroupreplaces the old timeline and its step popup.- Each chat has a session store. Readers pick the slice they need. The old whole-object read is gone, and every caller moved in this PR.
useQuestionPinowns the pin and the jump for the tutor chat and the explanation chat. The old follow-to-bottom code is deleted.- The PR is large (148 files, about 7,400 lines added and 4,400 removed). The store, the ordered reply and the pin replace three things every chat screen reads, so splitting them would ship half-moved callers.
Noticed, not fixed here
- At the weekly usage limit, the usage banner covers the chat header's buttons and the pinned question's first lines.
- Local checks (
test:changed,pr:check) skip some suites CI runs. One CI failure got past them on this PR. - On dev, a follow-up in the explanation chat sometimes ends with no follow-up suggestions.
- At the largest text size the learner's bubble breaks a word mid-word. Dev does it too.
- The streak celebration can open over the first chat between two answers.
- Two iOS end-to-end tests stop early: past chats (the row's test ID is lost inside its context menu) and the custom drill (at the Practice shortcut).