Each browser tab is considered to be a session, associated with one user. Each session's activity level is tracked separately, with a user being shown with the highest activity level of all of its sessions (where active is highest, then idle, then disconnected).
Each tab periodically sends its current status to the server - every minute if it has a working WebSocket, every 5 minutes otherwise. Current status is reset to active every time the tab gains focus, or there is a keydown or mousedown event. It goes idle if it has been more than 5 minutes since the most recent of those events. The tab sends a status of disconnected in the onunload event, but we don't depend on this.
Every time a session sends its status to the server, the server looks at all of that user's sessions to determine whether that indicates a change to the user's status. If it does, it broadcasts that new status to every person watching every board that user is on (this goes through the same framework that the rest of Trello's instant updates go through, and is too complicated to explain here).
The server also checks each session to see if its been too long since we've heard from that session (2 minutes if its a WebSocket session, 10 minutes otherwise) and removes the session if it has been (removing a session indicates it is disconnected).
There are a couple of optimizations hiding in there, but that's the main story.