A tab is a small thing with a long life
Before Voyager drew a single pixel, it had a model of what a tab is. The shell (AppKit on the Mac, SwiftUI on the phone) only draws that model and sends Dad's gestures into it. The model lives in VoyagerCore, the parent target that imports no WebKit, AppKit or UIKit, so every rule in this lesson runs under test against a fake engine. Swift Quest covers the language; this lesson is about the shape.
BrowserTab carries two things that never change: its id and its identity. Both are declared let. A tab is born in one identity (one isolated store of cookies and site data, track 5) and closes in it. Links and pop-ups opened from it stay there. Everything else is state that moves:
status: the last thing the page said about itself (address, title, icon, loading). It is kept while the tab sleeps, which is why a sleeping tab still has a name and a face.page: the engine's page, ornil. Nil means asleep.processEnded: the engine ended this page's process (a crash, or the system's memory limit for one page).contentRevision: a counter that moves whenever a read might belong to a different document, so anything reading the page can tell a stale answer from a current one.
Four states, one table
| State | What the tab holds | How it gets there | How it leaves |
|---|---|---|---|
| Awake | a page | opened, or chosen | the sleep policy, a close |
| Asleep | no page; its status and the page's place as bytes | the sleep policy, or a relaunch | chosen, or reloaded |
| Ended | a page whose process is gone | the engine reports the process ended | loads again by itself, once a minute at most (lesson 4) |
| Native | no page, and never asleep | Home (voyager://home) or Pippa herself (voyager://pippa) | going to a web address |
The native row matters more than it looks. isAsleep is page == nil && !VoyagerPage.isNative(status.url). Without the second half, Home and Pippa herself would read as sleeping tabs forever: the strip would draw their titles in the dimmed colour of a sleeping tab, and Home's list of open tabs would give them the moon icon. wake carries the same guard, so a native page is never handed a web view.
The window owns order and the front
BrowserWindowModel holds one window's tabs array and its selectedID. It is @Observable and runs on the main actor, so the strip and the toolbar redraw from it directly. Selection has a side job. When selectedID changes, the tab that went behind is stamped with the time (backgroundSince) and the one that came forward is cleared. That timestamp is the whole input the sleep policy needs in the next lesson. A private window is a flag on the model: every tab it opens is in the private identity, and nothing it does goes into the session.
The pinned group is a prefix
Pinned tabs are not a separate list. They are the front of the one array, and pinnedCount is how many tabs at the front are pinned. Every rule keeps that prefix whole:
- A tab opened from a link goes right behind its opener, but never inside the pinned group:
max(index + 1, pinnedCount). - Pinning moves a tab to the end of the pinned group; unpinning moves it to the front of the rest. Both are the same line, an insert at
pinnedCount, read after the flag has changed. - A drag moves a tab only within its own group. A pinned tab dragged past the edge stops there and stays pinned, as in Chrome.
- Close Other Tabs and Close Tabs to the Right spare pinned tabs.
- A restored session puts pinned tabs first with two filters, pinned then unpinned, each group in its saved order. The code comment there doubts that a sort would keep that order, but Swift's standard library does guarantee
sortis stable, so the filters are the plainer statement of intent, not the only safe one.
Close, and the way back
Closing a tab takes a snapshot first. Unless the window or the tab is private, or the tab never had an address, the snapshot goes onto a stack of the last 25 closed tabs with its old index. The tab to the right takes the front; if there is none, the last tab does. ⇧⌘T pops the stack. The reopened tab gets a new id, because the closed tab is gone and this is its place reopened. It keeps the identity, the pinned flag, the per-tab pause on sharing with Pippa, and the page's saved place, and it goes back to its old index clamped into its own group.
Duplicate Tab opens the same address in the same identity beside the original. Moving a tab to another window is a detach and an attach: the tab leaves as a snapshot and its page is let go. Attaching across privacy is where a rule bites. A private tab moved into a normal window takes the default identity and loses its saved place, and a normal tab moved into a private window becomes private the same way, because a move must never carry one store's history or sign-in into another.
⌘1 to ⌘9
Chrome's convention is that ⌘1 through ⌘8 pick a tab by position and ⌘9 is always the last tab. Voyager copies it, edge included: with four tabs open, ⌘7 does nothing rather than jumping to the last one. A test pins that edge, because a model that quietly clamps would be a different browser under Dad's fingers. ⌃Tab and ⌃⇧Tab step through the tabs and wrap at both ends.