Main window
Windows, the title row, tab bar, sidebar pages with spaces and their agents, page dots and Archived Sessions, Open Session, hover card, tab page, pane cards, zoom, overview, floating panes, find bar, notices and the palette
Windows
- Each window shows one machine (What a machine is). Two windows can show different machines or the same machine (D34).
- Ovvi opens only sessions the user opened. A window with nothing to
restore shows the new-window view: at launch, on File ▸ New
Window (⌘N), and when the app reopens with no window (a Dock click). A
window that macOS restores keeps its pages, and starts their sessions
that are not running (Restore). A window opened for a machine
(the machine menu, Add Machine) opens that machine's
default, which the user asked for. - Two windows on the same machine share the live data of each session. Each window keeps its own selection: page, space, tab, keyboard card, zoom and sidebar width.
- Until a window shows its first page or the new-window view (a restore reads herdr first), it already has its background and title row; at launch (every window macOS restores, or the first new one) Ovvi's icon (96 pt) shows in the middle meanwhile, until a window of the launch has its content. The content then fades in over 0.2 s (at once with Reduce Motion).
- A new window takes the size and place of the window last moved, resized or closed (not in full screen) while the app runs, and AppKit moves it down and right a little when it would cover a window exactly. Before any, it fills the screen's visible frame (the screen without the menu bar and the Dock), as a zoom does; it is not full screen. A window macOS restores keeps its frame.
New window
The view of a window with no page yet (SessionPicker), on the window
background, its column centered 23 pt above the window's middle:
- Ovvi's app icon (80 × 80 pt), then "Ovvi" (22 pt semibold) 8 pt under it, then 24 pt lower one glass list (the Open Session panel's look, 440 pt wide, its list at most 420 pt tall, then it scrolls).
- The list's field is "Find a session", with the keyboard. Typing filters the sessions (a machine's other rows stay only when its name matches; its New Session “‹typed name›” row always stays), and the machines with a matching session come first, so Return opens the first match; the arrows or the pointer move the light, a click opens a row, Esc closes the window.
- One group per machine, This Mac first, under the machine's name: its running
sessions, then its stopped ones ("Running", "Stopped"), then New Session,
which starts a session of a free name (or "New Session “‹typed name›”") on
that machine and opens it (in a run with
-session, only for a typedovvi-name). A pick opens the session as the window's first page; on This Mac a stopped one starts. - Each machine's sessions as last listed show from the first frame, and the
machine reads again behind them: its rows change in place
(Data). A remote machine that needs a
password again drops its known rows, once a read finds it needs one (the
launch after a connect shows them once, then Connect). A machine with none
known keeps one empty row's room; after 150 ms it shows "Reading sessions…"
with a spinner, so a quick answer shows none. Each machine reads on its own
and answers within 15 s, whatever the others do; then its row says "No
answer within 15 s", also over its known rows. A machine whose sessions
cannot be read shows the row that fixes it: Install herdr (a box; it
opens the page and installs, Install herdr),
Connect (
network.slash, "Not connected"; it opens the page and asks what ssh asks in this window), ordefaultwith the error (network.slashon a remote machine, a warning triangle on This Mac). - At the list's foot, always in view: Add Machine… (Settings ▸ Machines at its add form; the machine added connects in this window while it is still empty) and Edit Machines… (Settings ▸ Machines). The list is at most as tall as the window leaves room for (its height less 400 pt, at least 120 pt).
- A window that macOS restores before its pick shows the view again. A run
with
-session(or a test's-defaultSession) lists onlyovvi-*sessions, and its fix rows open that session, notdefault.
Title row
One row, 46 pt tall as in Rex, always the same (D45). While the sidebar is open, the traffic lights, the sidebar toggle and the machine button sit in a leading area as wide as the sidebar, so the first tab starts where the cards start, also during a resize. From left to right:
- The traffic lights, centered on the row at 23, 46 and 69 pt from the left, where Rex has them (Tab bar and Liquid Glass); full screen has none. The row's items start 91 pt in, so the sidebar toggle sits where Rex has it.
- The sidebar toggle, always shown outside the overview (⌃⌘S). It has no highlight when the
sidebar is open. The icon changes instead: an outline window with a sidebar
line while the sidebar is closed (
sidebar.left), and the sidebar part filled while it is open (sidebar.leftunderrectangle.leftthird.inset.filled). The sidebar slides in and out over 0.3 s, and does not slide under Reduce Motion. Each window keeps its own choice. Items in the row are 8 pt apart. - The machine button.
- The space button, only while the sidebar is collapsed.
- The tab bar: the selected space's tabs, whether the sidebar is open or collapsed.
- A spacer.
- The overview button (⌃⌘O).
- ⌘ (the command palette, ⌘K) and
+(new tab, ⌘T).
While the overview is open the row shows only the traffic lights and one × that leaves it (Overview).
Each title-row button shows the system tooltip with
the action's name and its shortcut, for example "New Tab ⌘T". So do the page
header's +, the footer's Open Session button and each card header button.
The window title is "‹tab title› - ‹page title›" (the page in view's sidebar title: the machine's name for
default), so the
Window menu and Mission Control show it.
Machine button
- The machine's icon alone: no chevron and no name. This Mac is a laptop when it has an internal battery, else a desktop.
- The menu lists the machines, each with its icon and name, and a checkmark on the current one. Then "Open Session…" (the window's Open Session panel, as ⌥⌘T, for the machine's other sessions), "Add Machine…" (Settings ▸ Machines at its add form) and "Edit Machines…" (Settings ▸ Machines): Settings is the one place that adds and edits machines (Add a machine).
- Picking another machine brings its window forward (the one used last, when
there are several); with none open, a new window on it opens with its
defaultsession. This window and its pages stay. Picking the window's own machine does nothing. - macOS 27 draws no images in these menu rows, so there only the names show.
Space button
Shown only while the sidebar is collapsed, after the machine button.
- An outline folder icon, then "‹page title› | ‹space›" (the sidebar page header's title, so the
machine's name for
default): the title in the title row's chrome color, a thin vertical line in the divider color, the space in the selected title color, a step brighter, all semibold. Then a chevron. At most 260 pt wide: the page title stops at 110 pt (with an ellipsis), and a space name that does not fit fades out at its end over 16 pt. - The menu starts with a filter field ("Filter or create…"). Then one group per page open in this window, in page order, under the session's name. A row shows the space's state icon (nothing for an idle agent or no agent), its name and its branch on the right. At the end, under the list and always in view: New Space, then "Open Session…" (⌥⌘T).
- The menu is a glass panel under the button, 300 pt wide, its space list at most 340 pt tall (then it scrolls), built from the palette's filter list. Rows are 26 pt. The arrows or the pointer move the highlight, Enter acts, Esc closes. A typed name that matches no space shows "New Space “‹name›”" first; Enter makes it in the session of the page in view and selects it.
- Picking a space of another page also shows that page.
Tab bar
The tab bar is always in the title row. The sidebar never lists tabs (D45). One capsule per herdr tab of the selected space (D42).
- Track. A capsule 34 pt tall sits behind the tabs. It is hidden when the space has one tab.
- Width. Every tab is 220 pt wide and never shrinks. When the tabs do not fit, the bar scrolls sideways (a scroll gesture over it), and a 24 pt fade shows on each end that has tabs out of view. A click selects a tab where it is, with no scroll; a tab selected another way (keys, the palette, Go Back) comes into view at once, without motion. A tab that opens in the space in view, however it opens (⌘T, a ⌘-click in the background, an agent, a plugin, another herdr client), slides into view over 0.22 s when it is out of view (at once with Reduce Motion). A tab or a pane dragged and held near either end of the bar scrolls it (the scroll view's own drop scroll). The bar makes only the capsules in and near its view, so a space with many tabs shows as fast as one with a few.
- New tabs. ⌘T,
+and a pane dropped on+or the bar's empty end make a new tab at the end of the space's tabs, as in Safari (⌘T,+and the Dock menu's New Tab only while the session answers: not while it is stopped, starting or reconnecting). An open from a card (⌘-click, Open in New Tab, Git's Open File), a link opened in a new tab, the palette's Open File, Open Working Tree and Open Commit, andovvi open --tabmake it right after the card's tab (the keyboard card's for the palette, the asking pane's forovvi open); so does Move Pane to Tab ▸ New Tab, right after the pane's tab; Archived's Resume right after the space's selected tab. herdr adds every tab last, and the app then moves it (tab.move). The bar shows the tab in its place from its first frame, never at the end first: the app keeps it there until herdr's order matches, or for at most two snapshots after herdr's answer to the move. - Dividers. 1 pt wide, 18 pt tall, centered between tabs. A divider next to the selected or the hovered tab is hidden, at once.
- Selection. One marker sits on the selected tab. It is 30 pt tall and fully round, 2 pt inside the track, as in Rex. Every switch (a click, +, ⌘T, ⌘1…⌘9, ⇧⌘[ and ⇧⌘], ⌃⇥, the palette, a close, an open from a card, the overview after its zoom) shows the tab's page at once, and the marker jumps with it: no slide, no fade. A new tab or a closed tab changes the bar at once too: no pill grows, shrinks or glides. A sideways swipe never changes the tab (Swiping). Glass draws the marker as a Liquid Glass capsule, Classic as a plain pill with a hairline edge (raised, with a shadow, in light).
- History. Go Back (⌃⌘←) and Go Forward (⌃⌘→), in the Window menu and the palette, step through the tabs shown before in the window's session, as a browser's Back and Forward step through pages: across spaces, past the tabs closed since. Showing a tab another way after Go Back drops the tabs ahead. The window keeps the last 100.
- Close. When the selected tab closes, the window selects the tab used before it (the latest one still open), else the tab on its left, else the new first tab.
- Hover. The hovered tab gets the hover fill over 0.14 s, the same shape as the selection; the whole 34 pt slot takes the clicks.
- Content. The program icons (11 pt in from the track's edge), the title (10 pt after them; the selected tab's in white 0.9 or black 0.9, 1 with Increase Contrast, the others in the title row's chrome color), and one status slot. A tab with several panes shows its panes' icons overlapping: up to 5, 4 pt apart, the keyboard card's icon in front. A tab with one pane shows one icon (Program icons).
- Title. One line. When it does not fit, its end fades out over 16 pt; it never shows an ellipsis. The folder part is in secondary text. The title is herdr's tab label. When the label is herdr's default (only digits), the title is the keyboard card's program and its folder's name, for example "zsh ~" or "pi ovvi" (D65). An agent's tab shows the agent's name and its pane's name in secondary text ("Claude Code Fix login"), a browser's "Browser" and its page, and a plugin's name and title ("File Explorer ovvi"). Until the program is known (a new pane's first second or so), the title is "New Tab"; a tab whose program cannot be read shows herdr's label.
- Plain-text titles. Where a title is plain text, with no dimmed part, a
|separates its two parts: "zsh | ovvi", "Claude Code | Fix login", "Browser | Hacker News". That is in ⌘K, the Window menu, Move Pane to Tab, the window title, a close dialog, the floating window's title and the accessibility labels built from them. The tab capsule, the hover card and the overview keep the dimmed part. - Status slot. × only on the hovered tab (a 22 pt hit area over the capsule's end, so the tab's width never changes); the title then fades out before it, never under it. Otherwise the state icon (12 pt) of the tab's strongest agent, centered where the × shows, on every tab with an agent that is not idle, the selected one too; the title fades out before it too. The icon never moves. An idle agent shows nothing (Theme and accessibility). VoiceOver reads the state in the tab's value: "codex, needs you".
- Announcement. When a tab that is not selected turns to needs you or finished, its icons bounce once (1 → 1.15 → 1, a spring) and its title shows "‹agent› needs you" or "‹agent› finished" for 3 s. A long text fades at its end like any title. Reduce Motion turns the bounce off.
- Context menu on a capsule: Rename Tab, Move Tab Left, Move Tab Right, Close Tab, Close Other Tabs, Close Tabs to the Right. The last two are also in the File menu and the palette, for the selected tab. Each asks once, with what runs and the total pane count, only when a program besides the shell runs in one of the panes. Rename (or a double-click anywhere on the capsule) puts a text field in place of the title: Enter saves, Esc cancels.
- Drag a capsule to reorder the tabs (Drag and drop).
- Rest the pointer on a capsule to see the hover card.
Sidebar
Flat on the window background, with no panel (D44). Arc-style pages, one per session open in this window (D43). One view (D111): each page has its header and its space rows with their agents (the only part that scrolls); under the pages, which they do not slide with, the page dots, Archived Sessions and the footer.
- Open sessions, not all sessions. A window starts with the page the user
picks in the new-window view (or the pages it restores). It
never loads other sessions by itself. The user adds pages with
Open Session, as many as they like. Only open pages, and a
session with a floating pane, have a
HostStore, snapshot reads and an event stream. - Session menu. A right-click on the page header or on the sidebar's empty area under the rows shows the session's menu: New Space, Open Session…, Show Archived Sessions (Hide while it is open; not there while Settings hides it), Show Only Pinned Spaces (Show All Spaces while it is on; disabled with nothing pinned, Pinned spaces), then Detach Session. New Space, Open Session… and Detach Session do what their menu bar items do; Show Archived Sessions opens the shelf, Hide folds it.
- Detach Session. The session menu and the context menu of its page dot have Detach Session (also in the File menu). It removes the page from this window. When no other window shows the session, its store disconnects, so its event stream and its terminal streams end. The herdr session keeps running and stays in the Open Session list. Detach Session is disabled on the last page. The UI says "session", never "page": Window ▸ Previous Session and Next Session show the page before or after.
- Restore. Windows come back after a normal quit too: Ovvi registers
NSQuitAlwaysKeepsWindowsin its own defaults, so the system's "Close windows when quitting an application" setting does not apply to it (a user's owndefaults writestill wins). A window that macOS restores opens the pages it had again, each with its selected space and tabs. A session that is not running (the Mac restarted) starts by itself, as its Start does; one deleted since is skipped (on a remote machine, only when it answers at launch); and a space or tab that is gone falls back to herdr's focus. A window never comes back in full screen: one that macOS restores in full screen leaves it. Only the installed app (make install,make dmg) has its bundle id, so no other build's windows take the place of the user's (Data). A restored window of a remote machine that needs an answer keeps its pages, which show Disconnected with Connect: nothing asks by itself. - Pages. A sideways swipe on the sidebar moves to the next or previous open page (Swiping); so do ⌥⌘[ ], and ⌥⌘1–9 show a page by its place (Window menu).
- Page dots. Over the pages' foot, above Archived Sessions, centered on the rows: one 6 pt dot per open page, 6 pt apart, 11 pt from Archived Sessions under them. The list's end has 18 pt more room for them, so its last row ends 11 pt above the dots. While rows lie under the list's end, its fade runs 36 pt instead of 18: the rows thin out from full to clear over it, and the dots lie over its faint end (rows at about a third behind them), in both styles, with no color painted. The current page's dot is in primary text color, the others tertiary. A click on a dot shows its page; its context menu has Detach Session. Hidden while one page is open. VoiceOver reads each dot as its page's title (the page header's).
- Page header. The session's name, the page's title (13 pt bold, the
rows' size, primary); for a machine's
defaultsession, the machine's name instead: This Mac's own name (System Settings, "Hari's MacBook Pro") or the remote machine's name from the list. The Open Session panel and the palette keep the real names. Its text starts where everything under it starts: 18 pt in from the window's edge, on the space rows' names and branch lines, the agents' icons and the shelf's label. It sits 7 pt above the first row (its baseline 16.5 pt above the row's top), and, at the trailing end, Sort Spaces (arrow.up.arrow.down, 11 pt semibold, so its glyph is as tall as the+'s, 10.5 pt) and a+that makes a new space in that session, in$HOME: two 28 pt buttons whose frames touch. The snapshot after it selects the new space. - Sort Spaces. A menu with a check on the current order of the unpinned spaces. The order is the session's,
in every window that shows it, kept in the sidebar's defaults (Data). A
session follows Settings ▸ Agents ▸ Sort spaces (Manual by default) until its own menu picks.
Manual is herdr's order, which a drag or Move Up and Move Down change; Recent puts the space whose agent
changed state last first (a start, a finish, a need: herdr's
state_change_seq, one counter for the session; a look at a finish is no change), the spaces with no agent after in herdr's order, a worktree with its repository's row. In Recent the rows hold still while the pointer is over them, and move 1 s after it leaves (a pick of the sort itself moves them at once, though the menu leaves the pointer over the list); the unpinned rows do not reorder (Move Up and Move Down are off on them), as while the filter hides rows. - Motion. Every change of the rows' order slides them to their new places in one motion, a 0.2 s ease-out (none with Reduce Motion): a sort, Move Up and Move Down, a pin or unpin, herdr's order, Recent's order, Show Only Pinned Spaces, and a drag's start and drop. A change from a menu starts once the menu has closed, whose fade draws no frame of it. A typed filter shows and hides rows at once.
- Pinned spaces. Pin Space and Unpin Space, in a space row's context menu and the palette (on the space in view), keep a space at the top: the pinned ones in herdr's order whatever the sort, then a 1 pt line (primary at 14 %) in the middle of a 20 pt gap, shown only while unpinned spaces follow, then the rest. Nothing says "pinned". A worktree goes with its repository (no Pin Space on its row). Pins are the session's, as herdr's order is, kept in the sidebar's defaults; a space herdr does not have drops its pin.
- The pin region. While a space is dragged, a row-sized region "Pin Space" takes the line's place (on top with nothing pinned), as the New Space ghost row does for a pane: a dashed 1 pt outline (primary at 14 %, radius 10) with secondary text, lit as a drop indicator (accent fill at 0.2, 2 pt accent stroke) while the pointer is over it. A drop there pins the space, last of the pinned ones (right after the last in herdr's order). A drop among the pinned rows pins it at that place; one under the region unpins it (in Manual at the drop's place; in Recent where Recent puts it). In Recent a drag pins, unpins and reorders the pinned spaces, never the rest. Not while the filter hides rows or only the pinned spaces show.
- Show Only Pinned Spaces, in the session menu and the palette (Show All Spaces while it is on), hides the
unpinned spaces but the one a window shows, so the user never loses their place; with no line or pin region. It
is the session's, kept in the sidebar's defaults, disabled with nothing pinned, and ends when
the last pinned space is unpinned. A hidden space is as if it were not in the session: its agents show no state,
count toward no Dock badge, are not in the Dock menu, and send no notification, neither from herdr's states nor
from
ovvi notify(Notifications). Show All Spaces brings their states back with no notification for what changed meanwhile. The next and previous space keys, ⌃⌘1–9 and the overview skip them too. - Order elsewhere. Next and previous space (⌃⌘[ ]), ⌃⌘1–9, the overview, Move Pane to Tab and the palette follow herdr's order with the pinned spaces first, less the hidden ones: the sidebar's in Manual. Not Recent's, which moves under the keys.
- Middle. The space rows, 10 pt apart. 18 pt fades show at the top and bottom of the space list when content is under them. When the selected space changes, however it changes (a click, the overview, ⌃⌘[ ], ⌃⌘1–9, a ⌥ + swipe, the palette, a notification), the list scrolls just enough to show its row, clear of the fade, without motion. The list is lazy: it makes only the rows in view and a few round them, so a session of hundreds of spaces and agents scrolls at the display's rate (without an app that reads other apps' windows, such as VoiceOver), and a swipe to its page draws in about a tenth of a second.
- Under the pointer. A space row's name line or an agent row fills at once, with no fade. While the list scrolls nothing fills; when it stops, the row under the pointer does.
- Empty session. A session with no space leaves its page quiet: no rows, no text and no button (the page
header's
+makes one). The window's content has the session's start (States). - Footer. The filter field and one button, Open Session (⌥⌘T), which
opens the Open Session panel. The filter keeps the rows
whose name or branch matches, and the rows with an agent whose name, agent, space or tab matches, unfolded to
those agents (a row kept by its branch alone stays folded). A matching worktree also keeps its repository's row.
With none, the compact empty state says "No matching spaces" (
magnifyingglass). Archived Sessions has its own search field. The footer has no New Space and no Add Machine: the page header has+, and the machine menu has Add Machine. - Width. 300 pt in a new window, from the window's edge to the cards: the rows sit 10 pt in from the window's edge, and the 8 pt handle lies over the gap before the cards. Drag the handle to resize it between 227 and 480 pt. Each window keeps its own width, also after a relaunch.
The sidebar never shows tabs, and its bottom never shows agent activity.
Space rows
One row per herdr space of the session, in the sidebar's order (pinned spaces first, then Sort Spaces' order), like herdr's Spaces panel.
- Line 1. The name (13 pt), 8 pt into the row, so the names, the branch lines and the page header's text start on one edge. So does the drag ghost "New Space". Nothing comes before the name. The name is medium weight while the row calls for the user (needs you or done).
- Chevron. A space that runs agents has a chevron (
chevron.right, 10 pt semibold, secondary) at the end of the name's line, 8 pt in; it turns down while the row is unfolded. A click on it folds or unfolds the row's agents and does not select the row; "Show Agents" or "Hide Agents" is its tooltip and VoiceOver label, "collapsed" or "expanded" its value. Rows are unfolded at first; each one the user folds stays folded, per machine and session, across relaunches (foldsAndShelves). The rename field hides it. While the footer filter holds rows open, the chevron does nothing. - State icon. At the end of the name's line, 8 pt in (6 pt before the chevron) and centered on that line: the state icon (12 pt, as a tab's) of the space's strongest agent, with no word and no time (Status). An idle agent, or no agent, shows nothing. The rename field takes its place while it shows. VoiceOver adds the state word to the row's value.
- Line 2, only for a git repository, under the name and starting where it starts: a branch glyph, the branch, then ↑ahead in the finished color and ↓behind in the error color when they are not zero. All secondary: the glyph 10 pt, the text 11 pt. There is no changed-file count. A space that is not a git repository has one line and no text about git (D67, Git status).
- Size. 44 pt with two lines, 30 pt with one, plus its agents when unfolded; 10 pt apart. The selected space is a raised pill (Liquid Glass in Glass), radius 10, over its agents too. Every other row has a 1 pt border (primary at 14 %, radius 10) round it and its agents, folded or not, and no glass.
- Worktrees. A space that herdr marks as a linked git worktree
(
worktree.is_linked_worktree) sits right after the space with the samerepo_keythat is not a linked worktree. It is indented 14 pt. A 1 pt guide line in the separator color crosses the gap above it, 24 pt in, about where its border's top turns straight (its 14 pt indent and 10 pt corner), and runs 1 pt into that border. With no such space in the session, it is a normal row. - Click selects the space in the app only (Selection and "seen" are the app's own).
- Context menu. New Tab, Rename Space, Move Up, Move Down, Pin Space or Unpin Space, Close
Space. A row with a git line also has Fetch, Pull and Push, between
separators before Close Space.
- New Tab opens in the folder of that space's keyboard card, or
$HOME. - Move Up and Move Down move a group past the next group pinned like it. A group is a space and its worktrees. Both are disabled on a worktree row (it moves with its repository) and at the ends of the pinned or unpinned spaces.
- Fetch (
git fetch --all), Pull (git pull) and Push (git push, which sets the upstream of a branch that has none) run in the folder of the row's git line, on the session's machine. git never asks for a password: it fails instead. While git runs, a notice with a spinner says "Pulling ‹name›…". When git ends, the git line is read again, and the notice turns to a green check and "Pulled ‹name›" for 2 s, or to git's own message in the error tone for 4 s. Pull and Push stay enabled at zero behind or ahead: the counts are from the last fetch, and Pull fetches first. - Close Space asks only when a program besides the shell runs in one of its panes, with what runs and the tab and pane count. On a repository with worktrees it also closes their spaces, and the dialog says so. The worktrees stay on disk.
- New Tab opens in the folder of that space's keyboard card, or
- Rename. Rename Space, or a double-click on the row's name or git line, puts a text field in place of the name: Enter saves, Esc cancels. An empty or unchanged name sends nothing.
- VoiceOver reads the branch with the ahead and behind counts, and "worktree of ‹repository›" for a worktree row.
- Drag a row to reorder the spaces, in Manual (Drag and drop).
Agent rows
An unfolded space row lists its agent panes under its name, inside its glass or border, 4 pt in from its edges.
- Order. Fixed: by tab, then by place in the tab's layout (left before right, top before bottom), never by state; the user does not change it.
- One line, 28 pt, one per agent pane, even when several share a tab: the program tile (8 pt into the space row, on the names' edge), then the pane's name (13 pt; its label, else its session's title, else its session's first typed prompt cut at 100 characters with "…", else "Untitled": Session titles); the row's width cuts it further. Then at the line's end the state box: the state icon (12 pt) in the state's color, on a 14 % tint of it with a 1 pt border at 45 % and 6 pt corners. Only a Working box adds a time after the icon: how long since the agent started working, 11 pt semibold in the state's color with tabular digits. It shows "<1m" for the first minute, then "1m", "2m", … and "1h 5m", and changes once a minute. Needs you shows its icon alone; a finish the user has not seen shows the green check (the unread mark) until the user leaves that pane. An idle agent shows no box. The name is medium weight for needs you and an unseen finish.
- Times. herdr gives no times. The app records when it saw each agent's state change, with its
state_change_seq, per machine and session, across relaunches: an agent whose number did not change keeps its time. An agent first seen after a launch counts from then. - Lit. The row of the shown tab's keyboard card has a soft fill (primary at 8 %, 7 pt corners).
- A click goes to that space, tab and pane and gives the pane the keyboard. Its finish counts as seen when the user leaves the pane. Rest the pointer on a row to see the hover card of its tab.
- VoiceOver reads "‹name›, ‹space› · ‹tab title›" and the state word as the value.
How it works: Agents and notifications.
Archived Sessions
A folded shelf at the sidebar's foot, under the pages and the page dots, above the footer (D110), 10 pt under the rows with one page, as two rows are apart. It is the machine's, not a session's: it does not slide with the pages, and every page shows the same. Settings ▸ Agents ▸ Show closed sessions (on by default) hides it, and then nothing is read.
- The shelf. Open, a 1 pt border round it (primary at 14 %, radius 10), as a space row that is not selected, and no glass; folded, no border, only its header. Its header is 30 pt tall: "Archived Sessions" (12 pt medium, secondary) 8 pt in, on the space names' edge, then at its end a chevron where a space row's is: right while folded, down while open. Under the pointer the header fills. A click folds or opens it; it is folded at first, and the fold is kept per machine across relaunches; a fold clears the search. It has no count (it is not known without reading every store).
- Search. Open, a search field ("Search",
magnifyingglass) sits under the header, 4 pt in from the border; a click that opens the shelf puts the caret in it. It keeps the sessions whose title or folder matches, among those read. A search reads the older pages by itself, 0.3 s after the last key, one after another, until every store was read or the list holds 1000 sessions; a new query, another agent or a fold stops it. Older matches land under the rows, with "Searching older sessions…" under them while it reads. Paging, not a grep on the machine, which would read whole transcripts. Esc folds the shelf, which empties the search, and gives the keyboard to the pane in view. At its trailing end a menu button (line.3.horizontal.decrease.circle, filled while it shows one agent, "Show Agent") opens a menu: All Agents, then the four agents. The choice is kept across relaunches. The footer's filter does not reach it. - List. Under the search field, one height whatever it shows, five rows' (256 pt): the loader, an empty state or a failure sits in its middle, rows scroll inside, with an 18 pt fade at an edge that has more rows, and the header and the search field never move. The spaces above get the rest, and scroll.
- Rows. 48 pt, two lines, 3 pt apart: the agent's tile, the session's title, its folder on line 2 (
~for the home folder), and its age at the end of line 1 ("5h", "2d", "1w"; 11 pt, secondary). Newest first, over the four agents. The tile is 8 pt into the border, on the space names' edge. Left out: the sessions a pane of any open session of the machine runs, and one being resumed. - Reading. The shelf reads each time it opens, 50 sessions with a prompt an agent a page (a page looks past the
sessions it leaves out, at most 2000 files, so a machine full of Agent SDK runs or
codex execruns still fills one), and shows the list once every agent's page came, or 2 s after the first one did: an agent still reading then comes in by itself, with "Reading sessions…" under the rows meanwhile. A page that takes over 10 s fails. The first time, the loader ("Reading sessions…", after 0.3 s) shows in the list's place; after that, the machine's last list that read whole (kept for the app's run) shows at once and the new one takes its place when it comes. The next pages read when the list's end scrolls into view, and come in together. One shell script an agent runs once a page on the machine (never a process per file), and nothing writes to the stores. Pi and Claude Code keep a JSONL file a session under~/.pi/agent/sessions/and~/.claude/projects/; the title is the last rename, else Claude Code's own title, else the first typed prompt. Codex and OpenCode keep an SQLite index, read withsqlite3 -readonly; without sqlite3 Codex reads its rollout files, and OpenCode's line says "OpenCode needs sqlite3 on ‹machine›." Left out: subagents and child sessions,codex execruns and Claude Code's Agent SDK sessions, archived ones, and sessions with no prompt. - States (States). While the first list or a next page takes over 0.3 s: the loader, "Reading
sessions…". With no row, the compact empty state: "No closed sessions on ‹machine›." or "No ‹Agent› sessions."
(
archivebox), or for a search "No sessions found." (magnifyingglass), "No matches in the last ‹count› sessions." once the search stopped at 1000. A machine that did not answer in 10 s: "Could not read ‹machine›'s sessions." (exclamationmark.triangle) with Retry, or, when only some agents did not, "Could not read ‹Agents› sessions on ‹machine›." (the failed agents, as "Codex and Pi") with Retry under the rows of the others. A store the machine cannot read shows its own line ("OpenCode needs sqlite3 on ‹machine›."). - Resume. A click, or the hover Resume (
arrow.uturn.backward, before the age), starts the agent with that session (the flags of Relaunch with Chat's table) in a new pane in the session's folder: a new tab after the selected tab of the space whose folder holds it (the deepest; a folder two levels deep or less, as/or/Users/me, holds only itself), else a new space there. ⌥-click, and the menu's Resume to the Side, split to the right of the keyboard card instead. The menu has Resume and Resume to the Side. A row resumes once: it leaves the shelf until its start ends. A folder that is gone says "‹folder› is gone" in a notice (Pi resumes in the home folder: it keeps its own folder), and an agent the machine does not have says "‹Agent› is not on ‹machine›".
Open Session
The one place to create or open a session, as a page of this window (D68). The footer's Open Session button, the session menu's, the space menu's and the machine menu's "Open Session…" and File ▸ Open Session… (⌥⌘T) open it.
- A glass panel like the command palette, 420 pt wide, near the top of the window, with no title and no buttons: Enter acts, Esc or a click outside closes it. One panel shows at a time: opening it closes the command palette or the space menu, and opening one of those closes it.
- A text field, "Name a new session, or find one", empty and with the keyboard at first.
- The list, filtered by the field. It shows at once the sessions the page
last read (as it connected, or as the panel last opened), then reads them
again; a change updates its rows in place. It lists only the window's
machine's sessions (the page's own store, one per machine and session).
Groups with the headers "New", "Running", "Stopped" and "Open in this
window"; in a remote window "Running on ‹machine›" and "Stopped on
‹machine›", as both machines often run the same names (
default):- New: "Create session “‹typed name›”" when the text is a valid herdr session name that no session has. With an empty field: "Create session “‹name›”" with a suggested two-word name.
- Running sessions that are not open in this window, then Stopped sessions.
- Last, the sessions already open in this window.
- The rows carry no hint text: the group headers say what a pick does. A small symbol leads each row: a plus to create, a stack for a running or stopped session, a checkmark for an open one.
- The arrows or the pointer move the highlight, in the same frame and with no
fade, as in a menu (a row scrolled under a still pointer does not take it).
Enter or a click acts on a row.
- A new name, a running session and a stopped session take one path. The
app starts the server if it is not running (a launchd job on This Mac,
D47); a server the app started with no space gets its first space in
$HOME. Then the session's page opens last in the window's pages and shows, and its store connects. - An open session: its page shows.
- A new name, a running session and a stopped session take one path. The
app starts the server if it is not running (a launchd job on This Mac,
D47); a server the app started with no space gets its first space in
- Stop and Delete. The highlighted row of a running session (not open in this window) has a Stop button at its right end: the server stops, and every program in it ends; its spaces and tabs stay for its next start. The highlighted row of a stopped session has Delete: its spaces, tabs and panes go for good. Both ask first ("Stop the session “‹name›”?", "Delete the session “‹name›”?"), as a close does (D41). The list is read again after each, and the light stays on the same session. Both buttons are red.
- Detach. The highlighted row of a session open in this window has a plain Detach button, as File ▸ Detach Session: its page leaves this window and the session keeps running, listed again under Running, and the light goes to the list's first row. Not on the window's last page.
- While a server starts, stops or is deleted, its row shows a spinner. A failure shows its text in the panel, with Retry, and the panel stays open.
How it works: Checks on connect.
Hover card
A peek at what runs in a tab (D63). The overview shows every tab at once.
- When. The pointer rests 0.25 s on a title-bar capsule or an agent row. The card stays while the pointer moves into it (0.15 s grace). It goes when the pointer leaves, on a click elsewhere, on a scroll, and with Esc. One card at a time, and none during a drag.
- Where. Centered under a capsule, 6 pt below the title row, or 6 pt right of the sidebar edge for an agent row. It stays inside the window.
- Look. A glass panel (floating glass, see Theme), radius 12, 280 pt wide. It fades and grows in from 0.97 over 0.13 s.
- Content, top to bottom:
- The tab title (folder part dimmed), with no state.
- One row per pane, at most 6 and then "+N more": the program glyph, the program or agent name, the folder in
secondary text; a plugin's, a browser's and New Pane's row has its icon, its name and what it shows (the file,
folder, diff or page), as its card header, never its placeholder's
cat. For an agent that is not idle, its state icon (12 pt) on the right, with no word. Other panes show nothing there. Every line is one line: the title, a first prompt's too, fades at its end. - For a pane that needs you: the last 3 non-empty lines of its screen, in
secondary monospaced text. The app reads them with
pane.readwhen the card opens, one call per such pane, and shows nothing if the read fails.
- A click on a pane row selects the space and tab and gives that pane the keyboard. A click on the title selects the tab.
- VoiceOver: the card is a panel in the window with the rows as buttons. A tab's action "Show Panes" opens it.
Tab page
- The window shows the selected tab's page, full width, and only it. A switch replaces it at once (D12).
- Kept tab pages. The pages of the 8 tabs shown last, over all the session's spaces, stay made under the shown one, hidden. A switch to one of them shows it as it was and builds nothing: a chat keeps its rows (Show Earlier's too) and its scroll place, and a web card stays in the window. In a hidden page the terminal and web views are hidden too, so no mouse move, cursor or hover reaches them, and they give up the keyboard, as do the cards' text fields. A hidden page's cards take no drops, so a file or a pane dropped on the shown page reaches its card. The overview keeps them too, all hidden. A tab past the 8 or a session switch makes its page again. A chat's view, each time it is made (its tab's first show, the flip from the TUI, a floating window), draws its rows on the next run loop turn, so the switch draws the tab and the card frames in the click's own turn.
- When the page's size changes (the sidebar, the window), the selected space's other tabs' kept terminals, and every kept tab page's terminals, take their card's new size once it settles, while hidden, as Ghostty's hidden tabs do: their programs hear of it then, and a switch to the tab shows no reflow.
- A pane the app creates shows the New pane chooser first, unless Settings ▸ General ▸ New panes open names one kind.
- Inside a page, pane cards follow herdr's split tree, with an 8 pt gap between them. The gap is the divider: drag it to resize (a grip shows on hover and while dragged), double-click it to reset it to 0.5, and ⌥⌘= balances the tab. With Balance splits automatically on (Settings ▸ General, the default), a pane that opens or closes leaves the panes in its row or column equal shares (Layout).
- Layout changes animate over 0.13 s: a new card slides in from its side, a closed one fades away, a zoom grows the card to the page and back. A divider drag follows the pointer.
- Browser and plugin cards are made as their tab first shows (Pictures and switches).
How it works: Layout and sizing.
Pane card
-
Card. Radius 10. Its terminal fades in once herdr's frames settle, and at each new grid it keeps its old picture until they settle again (States).
-
Header. A terminal card's header is one row, 36 pt tall. 11 pt in from the leading edge: the program glyph (16 pt, secondary), 3 pt before herdr's pane label if set, else the program's name and the folder in secondary text, for example "vim ~/code" or "zsh ~" (Program icons). 8 pt in from the trailing edge, five 22 × 22 pt buttons, 2 pt apart: Float Pane, Split Right, Split Down, Zoom and Close, with 11 pt regular glyphs, dimmer than the title row's buttons (white 0.42 or black 0.45, at least 3:1 on the card). Zoom is disabled when the tab has one pane. A zoomed card shows only Restore Pane and Close. While the New pane chooser shows, the header says "New Pane".
-
Two rows. A browser card, a plugin card whose manifest has a
headerorhistory, and an agent card (herdr reports an agent, or the pane has a chat's launch marker) add a second row, 32 pt, for their kind's controls (D80). Row 1 keeps the title and the card buttons, so it lines up with a terminal card's header beside it. Row 2 ends where row 1 does. Under a browser's or a plugin's row 2, 2 pt more stay before the page, and the browser's page starts 4 pt lower still: its 22 pt buttons have 11 pt under them to the content, as in a floating window.- Browser: row 1 has the page's icon and title, row 2 the toolbar (Browser).
- Plugin: row 1 has the page's symbol, the title and the Edited dot for unsaved edits, row 2 Back, Forward and the page's controls (File Explorer and Git).
- Agent: row 1 has the pane's name: its label, else its session's title (Session titles). Row 2 has the program glyph, the agent's name and the folder in secondary text ("Claude Code ~/code/ovvi"), then the agent's state label (its symbol and word in the status color, without the time) (herdr's, or the chat's own on a chat card; none while idle), then on the right Chat | TUI (Chat view), or Relaunch with Chat (Relaunch with Chat).
- A card at the chooser and a floated card's placeholder keep one row. An agent started by hand gets its second row when herdr reports it, and the terminal gets 32 pt shorter then.
-
Terminal. 10 pt in from the card's sides and 8 pt above its bottom; its text starts there (libghostty adds no padding).
-
Active border. When the tab shows two or more cards, the keyboard card has a 1 pt border inside its rounded edge, in the label color at 30 % in dark and 22 % in light. A tab with one card, a zoomed card, a floating window and the overview have none. The border moves with the keyboard at once, with no fade.
-
A click anywhere on a card makes it the keyboard card: on its terminal, its page, its header, a button, an error, a loader or an empty state. A view that takes the click's keys (a text field, a web page) keeps them; else the card's own view takes them (its terminal, its page or the chat's reply box). A card with no such view (a missing plugin) takes them from the other card. A floated pane's placeholder is left alone: Show Window raises its window. A click in a floating window gives its pane the keys when no view there has them.
-
The buttons show on the keyboard card, on a hovered card and on a zoomed card.
-
Selecting. A left drag, a double click, a triple click and a ⇧-click select text in the card. A plain left drag (no modifier key) also goes to the program, so a program that reads the mouse (pi, for one) selects and copies on its own. A ⇧, ⌥ or ⌃ drag and a double or triple click select in the card alone: the program gets such a single click when the button comes up on the cell where it went down. A ⌥-drag selects a rectangle. On a watched card, a plain press takes control at once. A ⇧, ⌥ or ⌃ click takes control when the button comes up.
-
Known limit: white cells beside a selection. A program that selects text on its own (pi) shows its selection with the text and background colors swapped. herdr draws that black on white. The card's blue selection covers it where both select the same cells. A cell that only the program selected shows white:
- an end cell, because the card takes an end cell by Ghostty's 60 % rule and the program takes it whole;
- for one frame after each move or click, because the card's selection changes at once and the program's comes back through herdr;
- when the program's text scrolls under its selection.
herdr does not tell the app which programs read the mouse. When it does, a plain drag will go only to such a program, and a ⇧-drag only to the card, as in Ghostty.
-
Context menu on the header, and on the terminal, where it starts with Copy, Paste and Select All. As in herdr, a right click in the terminal opens the menu even when the program reads the mouse. ⌥-right click sends the click, without the ⌥, to a program that reads the mouse, and does nothing in one that does not (herdr's
right_click_passthrough_modifier = "alt"). herdr's per-pane setting that gives a pane's right clicks to its program is not in its API, so the app does not follow it:- Float Pane, Split Right (⌘D), Split Down (⇧⌘D), Split Left, Split Up, Zoom Pane or Restore Pane (⇧⌘↩).
- Move Pane to Tab ▸ (New Tab, right after the pane's tab and disabled for a pane alone in its tab, and New Space, a separator, then every tab of the session but the pane's own, grouped by space: each space's name as a section header, in herdr's order: the sidebar's in Manual), Rename Pane…, Reset Pane Name.
- On a plugin card: Reload Plugin (⌥⌘R) and Show Log.
- Redraw Terminal (⌥⌘R, not on a browser or plugin pane), Replace Pane (⇧⌘R), Open Terminal Here (on an agent, browser or plugin pane), Start Agent Here ▸, Open Browser Here, Open File Explorer Here, Open Git Here (each but the pane's own kind, New pane), Close Pane (⌘W).
-
The same items are in View ▸ Pane in the menu bar, for the keyboard card (the zoomed card of the selected tab, when there is one). Redraw Terminal, Replace Pane, Open Terminal Here, Start Agent Here ▸, the Open … Here items and Close Pane are in the File menu. In View ▸ Pane, Move Pane to Tab ▸ has New Tab and New Space only: AppKit searches the whole menu bar for a shortcut at every key (about 30 ms at 300 tabs). The card's menu and the palette list the tabs.
-
After a move, the window selects the tab the pane went to, and the pane keeps the keyboard.
-
Rename Pane puts a text field in the header. Enter saves the label; an empty name clears it; Esc cancels. Then the keyboard goes back to the card. Reset Pane Name (enabled once the pane has a name, not on a browser or plugin pane, also in the palette) clears herdr's label, so the header shows the program again.
-
Drag the header to move the pane (Drag and drop). A click on the header's empty part gives the card the keyboard, as a click on its content does.
-
Replace Pane (⇧⌘R, also in the palette) puts a fresh shell in the pane's place, for example after a change to the shell's setup. The new pane has the old one's place and size, starts in its folder, and gets the keyboard. The old card stays until herdr has closed the old pane; then the card shows the new terminal in place, with no slide or layout motion: only the terminal's usual fade-in plays. On an agent, browser or plugin pane the new pane shows the New pane chooser; on a terminal it does not. A zoomed pane's new pane stays zoomed, and a floated pane's new pane floats in a window at the same place, with its Stay on Top. ⌘W while a replace runs does nothing: the old pane is closing already.
-
Redraw Terminal (⌥⌘R, also in the palette) makes the program in a terminal pane paint its screen again, for when other text got drawn over it (a TUI's input box under warning lines after the Mac slept). herdr has no call for it, so the app makes the pane one column narrower and then its own size again: the program gets two size changes (SIGWINCH), and pi, Claude Code and Codex draw their whole screen at a new width. The card shows its old picture until the program has drawn at the card's size again, so the narrow frames never show. A plain shell draws only its prompt again. Not on a pane that another window controls (a watcher cannot resize), nor on a pane whose card shows the chat view.
-
Close and Replace Pane ask first unless the pane runs only its shell or a browser or plugin pane's placeholder (Return confirms, Esc cancels). Replace Pane's dialog says "Replace “‹name›”?" and has a Replace Pane button.
-
Every question before an action that ends something (a close, Replace Pane, Stop or Delete Session) shows that action's button in red, the macOS destructive style. AppKit draws a red button only when it is not the dialog's default button, so the app gives Return to the red button itself.
-
Reveal. When an agent runs
ovvi revealon a card in view, the card's border lights in the accent color, 2 pt, for 0.6 s, and fades out over 0.25 s (CLI). A floating card flashes in its own window. A card out of view gets a notice instead.
Opening links and files
A link or a file opens the same way in every card that shows one: File Explorer's tree and its Markdown, Git, the browser, the chat view and the terminal.
| Gesture | Opens |
|---|---|
| a click | in the card, when the card shows that kind of place itself; else in a new tab, which is selected |
| ⌘-click | in a new tab, in the background |
| ⌥-click | in a pane to the side of the card, in the background |
| a right click | a menu of Open, Open in New Tab and Open to the Side, which do what the three gestures do |
| ⌘⌥-click | a link in the terminal or the chat, or a file, folder or link in File Explorer and Git (not a commit): in the Mac's default app for it, outside Ovvi |
- What a card shows itself. File Explorer shows files and folders (a file from its tree, a relative link in Markdown). Git shows its home and its diffs (a change, a commit, a parent). The browser shows web pages. So a web link in Markdown or in a chat reply, and a file from a Git diff, open in a new tab on a click.
- HTML. A link to an HTML file (
.html,.htm) on This Mac, in the terminal or in Markdown, opens the file's page in a browser pane (afile:page). File Explorer's tree and a Git diff open the file as text, and a file card on HTML has Open in Browser (globe) in its header, which opens the page in a browser pane in a new tab, selected (A file's row 2). On a remote machine HTML opens as a file, since a remote card never shows a file overfile:. - The look. A link looks the same under the pointer with ⌘ or ⌥ down as with neither: in Markdown (locked, and unlocked off the caret's line) an underline and the pointing hand, in the browser the page's own look. The terminal, where only ⌘-, ⌥- and ⌘⌥-click open a link, shows the link under the pointer and the pointing hand only while ⌘, ⌥ or ⌘⌥ alone is down, the same for each. ⌃, ⇧ or another pair of keys shows none. The link gets a tint (the label color at 0.12, 4 pt corners, 2 pt past its ends, one box a row), not an underline: it leaves the text's own underline, colors and selection as they are. Only the hovered link shows. An OSC 8 link shows libghostty's own hover underline instead.
- In the background means that the selected tab and the keyboard stay where they are: a ⌘-click on one link after another opens each one. The menu's Open in New Tab and Open to the Side open in the background too.
- A new tab is in the card's space, on the same machine (New tabs). To the side is a split to the right of the card.
- From a floating card (Floating pane), a link that would open in a new tab or to the side opens in a new tab after the card's tab, floated in a window of its own: ⌘-click and ⌥-click do the same there, in every card, the browser too.
- Lists select first. In File Explorer's tree and Git's commit list a click selects a row and a double-click opens it (in the tree also ⌘O, in Git's list Return), as in the Finder. ⌘-click and ⌥-click open at once.
- Markdown, unlocked, opens a link on a click too, as locked, and shows it under the pointer the same way. On the caret's line a link shows its source, and a click there puts the caret in it. A Markdown link's menu ends with Copy Link (the file's path or the address).
- The browser's menu on a link is WebKit's Open Link, then Open
Link in New Tab and Open Link to the Side. A
target=_blanklink opens a new tab in the background (Popups, links and dialogs). - The chat's links are what the terminal finds, in the messages, their code and a tool call's Input and Output (Markdown), and they open as the terminal's do: ⌘-click in a new tab and ⌥-click to the side, both in the background, and ⌘⌥-click in the Mac's default app. A plain click opens as a browser's link does, in a new tab, selected: chat text is no program's, so the click is free. A ⇧- or ⌃-click elsewhere is the text's (it selects or opens the text's menu); on a link it does nothing. A link has no menu of its own: SwiftUI's text has none, so the right click shows the text's menu.
- A web page opened from Markdown or the chat follows the browser's load rule (When a page loads by itself). One opened from the browser is approved: the user picked it there.
- A terminal's links are what the app finds in the text on the screen,
and OSC 8 links (libghostty finds and underlines those; its own search
for addresses and paths is off). A terminal has no Open: a plain click is
the program's. ⌘-click opens a link in a new tab and ⌥-click to the side,
both in the background. ⌘⌥-click opens it in the Mac's default app for
it: a web address in the default browser, a file in its app, a folder in
the Finder, as a double-click in the Finder does (an app or a script
runs). A file on a remote machine, or its own address (
localhost), opens nothing on the Mac and shows the notice "‹path› is on ‹machine›, not on this Mac". A ⌥-click anywhere else is still the program's (a shell moves its cursor). The right click stays the card's menu, with no link items. The card's accessibility title is the link under the pointer.- Addresses.
scheme://…(with a query, a fragment, a port, an IPv6 host in brackets,localhost),mailto:and emails (me@example.com). A name with no scheme (example.com) is no link: it reads as a file name too (package.json). A web address opens a browser pane, an email the Mac's mail app. Other schemes (ftp:) open nothing and show the notice "Ovvi can't open ‹address›"; with ⌘⌥ they open in the Mac's app for the scheme, when there is one. - Paths. Absolute,
~/,./,../,file:, and relative paths (src/main.rs) resolved from the card's folder, the folder herdr reports. An absolute path needs two names (/usr/bin), an extension (/notes.md) or a closing/(/tmp/), so slash commands (/compact,/help) are no paths. A relative path needs a/and an extension, soand/or,1/2and2026/11/11are none. A space belongs to a path only between quotes ('/tmp/my file.txt') or escaped (my\ file.txt). A:lineor:line:columnafter the path, tsc's(line,column)or Python's", line 12lands on that line. In a diff line (--- a/,+++ b/,diff --git) thea/andb/are dropped, and a mention's@(@src/a.ts) too. A quoted sentence (-m "fix a/b.swift") keeps its own paths;$HOME/a.txtis none. - Ends. A link ends at a space and before the punctuation after it:
.,,,;,:,!,?, a closing bracket or quote it did not open (a Wikipedia address keeps its own(…)), and Markdown's**,_and backticks.Read(/tmp/a.txt)is the path alone. - Wrapped lines. A row whose last cell holds text goes on in the next row, so an address wrapped across rows is one link (herdr's frames carry no soft wraps, so a program's own full-width row joins its next too).
- Nothing there. A path opens only when a file or folder is there on the card's machine (a remote machine checks there). Else the click shows the notice "File not found: ‹path›", with a relative path joined to the card's folder. A ⌘-, ⌥- or ⌘⌥-click on a link is never silent.
- Markdown opens in File Explorer's Markdown, HTML as above, other files with their default plugin.
- libghostty reports an OSC 8 link only while ⌘ is down, so the app gives libghostty ⌘ in place of ⌥ and ⌘⌥: with the pointer's moves while they are down, and when they go down or up with the pointer still (first off the view: libghostty looks again only on a new cell).
- Addresses.
- A
mailto:or an app's link in a page opens its app. In the chat, an email ormailto:opens the Mac's mail app; another scheme shows the notice "Ovvi can't open ‹address›", as in the terminal. - A file that is gone. A click, ⌘-click, ⌥-click or menu item on a file
or folder that is gone (a terminal link, a Markdown link, a Git
diff's file, a row of File Explorer's tree, ⌘K's Open File,
ovvi opentyped in a shell) opens nothing and shows a notice in the warning tone: "File not found: ‹path›", with the home folder as~.
Find bar
⌘F (Edit ▸ Find…) opens the find bar on the keyboard card; ⌘F again focuses its field. It is disabled on a browser card, on a card that shows its chat view, and on a plugin card that shows Preview or the Git home. On File and Markdown ⌘F, ⌘G and ⇧⌘G use the editor's own search, and on the tree and a Git diff the page's filter (File Explorer and Git).
- A glass capsule 320 pt wide and 32 pt tall at the card's top right, 8 pt in from its edge and 8 pt under the header: the field ("Find"), the match count ("3 of 12", in a fixed slot so the bar never changes width), ↑ (Find Previous), ↓ (Find Next) and × (22 pt buttons with 11 pt regular glyphs, as the card header's). One bar per window page, on the card it opened on.
- A new query selects the last match, nearest the bottom. ⌘G or Enter goes to the next match down, ⇧Enter or ↑ to the previous one up, round the ends. Esc or × closes the bar and gives the keyboard back to the card. As in Rex, Find Previous has no shortcut here: ⇧⌘G is Go to Directory.
- The text comes from herdr, read when the bar opens and again at the next step after Return was pressed in the pane, and the search is case blind. Each match scrolls herdr's viewport to it, with the match line centered. Closing the bar scrolls back to the bottom. herdr's viewport is shared, so a herdr TUI on the same pane scrolls too.
- The matches on the screen are highlighted: the selected one in the accent at 0.45, the others at 0.2, placed from herdr's visible text after each scroll and again 0.3 s after the pane's frames stop (at most 1 s of waiting).
- The bar closes, and herdr scrolls back to the bottom, when its card leaves view (another tab, space or page, or another card zoomed) and when its page or window closes. A failed scroll shows a notice.
How it works: Scrolling, queries and links.
Zoom
- The zoom button, a double-click on the header, or ⇧⌘↩ zooms a card. It fills its tab's page, as if it were the only pane. Only the zoomed card resizes; the others stay, hidden, at their size.
- A zoomed card shows only two buttons: Restore (the accent at 0.6 on an accent circle at 0.07, as in Rex; no saturated blue) and Close.
- A split from the zoomed card restores the layout first, so the new pane shows. Moving the keyboard to a hidden card restores it too.
- Restore or ⇧⌘↩ restores the layout. Esc is not used, because the program in the pane needs it.
Overview
Every tab of the session at a glance, as pictures (D70): for watching many agents at once.
- Open and close. The title-row button (
square.grid.2x2, after the tab bar, before ⌘), View ▸ Show Overview, the palette, and ⌃⌘O toggle it; Esc and the title row's × leave it. Each window has its own; it does not come back after a relaunch. - What it hides. The sidebar, the tab bar and every title-row button: the row keeps only the traffic lights and, at its end, one × (Close Overview, ⌃⌘O) that leaves the overview. ⌘K still works. The overview covers the window below the title row. It appears and goes at once, with no fade or zoom.
- Canvas. The session in view, scrolled vertically. ⌥ + swipe sideways,
⌥⌘[ ] and the palette step to the window's previous or next session, which
the overview then shows; a sideways swipe alone goes nowhere. From the top:
the session's title as the sidebar's
page header has it (13 pt semibold; the machine's name for
default), then the session's spaces in herdr's order, pinned ones first, less the hidden ones (the sidebar's in Manual), 32 pt apart: each space's name (13 pt semibold, secondary), then its tabs as tiles in a grid, 2 to 4 a row (as many 340 pt tiles as fit), 16 pt apart. Wide margins all round: 8 % of the window's width left and right (at least 48 pt), 40 pt above and below, so the grid sits centered. - Tile. One card per tab (the card's fill, rim and edge): at its top a 36 pt title row like a card header (the tab's icon stack, its title with the folder dimmed, and its state label), and under it, 8 pt in, a miniature of the page: the tab's cards in herdr's layout, at the page's real proportions (the tab's page as it shows when the overview closes), each card its fill and edge with its content's picture inside, at the place and size it has in the card (no header content: the tile's title row names the tab). The picture is the terminal's snapshot, or the card's last picture for any other card: a browser's page, a plugin's content, a chat (its kind's symbol until the card has a picture). A zoomed tab shows its zoomed card. A floated pane's card shows the float icon. The selected tab of each space has the keyboard card's look. On hover the title row shows × at its end (Close Tab, as the tab bar's ×: it asks only when a program runs).
- Click. A click on a tile selects its space and tab and leaves the overview: the tile's miniature grows to the tab's page over 0.18 s (ease out) while the other tiles fade out, and the sidebar and the title row fade in over 0.12 s; then the page shows. The sidebar and the title row stay laid out under the overview, hidden, so they come at once in a big session too. Esc, the title row's × and ⌃⌘O leave the same way, on the selected tab. The overview leaves at once under Reduce Motion, or when the selected tab's tile is scrolled out of view. It opens at once. A notification or Dock click leaves it at once and shows its pane. Other keys and the palette act as usual and the overview stays: ⌘T adds a tile, a tab key moves the selected tile.
- Keyboard. While the overview is open no terminal has the keyboard (a terminal view in a miniature refuses it); Esc leaves, unless a panel (the palette, Keyboard Shortcuts) or a text field is open. Closing it gives the keyboard back to the tab's card.
- VoiceOver: each tile is a button named by its tab's title, after its space's name.
How the miniatures draw
- The tiles draw pictures, not live views, so the overview costs the window almost nothing (D70).
- Taken as the overview opens for the cards in view: each terminal's snapshot, each web card's picture (Pictures and switches) and each chat's picture. The other cards keep the picture from their last show. A chat's picture is also taken as its card leaves view, from the window's layers at its place: the deepest layer that holds the chat's rows, under 1 ms, without the reply box.
- A terminal tile with no snapshot yet shows a spinner and streams until its terminal drew, then lets go and keeps its snapshot. At most 16 such cards (6 on a remote machine) and 400 MB of surfaces stream at once, only the tiles in view, once a scroll ends. Nothing turns live or lets go while the grid scrolls. These are not visits (D46). The terminals' snapshots are kept up to 100 MB.
- A web card's or a chat's tile shows its last picture, else its kind's symbol (
globe,bubble.left.and.bubble.right). A tile makes no web view, proxy or transcript read, and takes no web view from its card. - A tile's terminal that streams draws live, scaled by AppKit (a layer transform): libghostty keeps its size and grid, and herdr's PTY keeps the card's size. The tile takes the terminal's view from its card in the kept tab page under the overview and gives it back when the tile goes.
- Cost: a live card keeps its surface at full card size (its swap chain: What streams), which is why the cap is in megabytes. Tiles cannot get smaller surfaces: libghostty-spm takes its content scale only from the window.
Floating pane
A pane can float in a small window of its own, above the main window or, kept on top, above other apps (D50). herdr has no floating pane, so this is Ovvi's own: the pane stays in its herdr tab, and other devices and herdr clients see it there.
- Float Pane (the
pip.enterbutton in the card header, the pane's menus, View ▸ Pane and the palette) opens the window. Several can be open. On a floated pane's card the button is lit, and it brings the window to the front. - The window has no tab bar and no sidebar. Its title row is the main
window's: 46 pt, with the traffic lights where the main window has them.
It holds the traffic lights, then the pane's quick info, a thin vertical
line, then the place, "session › space › tab", in 11 pt tertiary text,
quieter than the pane's folder, on one line (the
session as the sidebar names it, so
defaultshows the machine's name; a long one gives way first, in its middle). The quick info is the pane's icon, its title and its agent's state label. The title is the program and folder or the label, an agent's name and its pane's name, "Browser" and the page, a plugin's name and title with its Edited dot, or "New Pane" while the chooser shows, with the second part in secondary text (the window's title has|there). A plugin pane (anyovvi:label, such as a browser) shows no line and no place. An agent pane shows the same agent control as its card after the place: Chat | TUI, or Relaunch with Chat, whose loader and chat show in the floating window. Then on the right the title row's 28 pt buttons: Stay on Top (a pin), Close Pane (closes the pane, and asks first as any close does, in the floating window, also for a Close from the card in the tab) and, rightmost, Detach Pane (pip.exit: the window closes and the card shows its terminal in its tab again; nothing stops). The red traffic light detaches too. Below the title row, the glass card holds only the terminal, with no header, 8 pt in from the window's edges, its text 10 pt in from the card's sides, as in a pane card. A chat pane's card shows its chat or its terminal (Chat view), a browser pane's card its toolbar and page (Browser), a plugin pane's card its row 2 and page (File Explorer and Git), and a pane at the chooser the chooser. A browser's toolbar and a plugin's row 2 start 4 pt below the card's top edge (only in a floating window), so their first button sits as far from the card's top edge as from its left, 11 pt. The same 11 pt stay under the buttons: the browser's row has 4 pt under it, and every plugin page starts its content 4 pt down (Rich Text's column keeps its 28 pt margin). The window is at least 320 pt wide, and never narrower than its title row needs (a chat pane's switch or Relaunch with Chat makes it wider). A pane's first float opens it at 60 % of the main window's width and 75 % of its height (at least 720 × 460 pt), centered over the main window, on that window's screen also when another screen has focus; it moves and resizes like any window, and keeps its frame for the next time that pane floats. - Stay on Top keeps the window above every app's windows and on every macOS Space (Mission Control), like picture in picture. It stays on until turned off.
- In the tab the card keeps its place in the splits and its header. In place of the terminal, in the middle of the card: "This pane is open in a floating window." in secondary text, and under it two buttons side by side: Show Window brings the window to the front, Bring Back Here detaches the pane (as Detach: the window closes) and gives its card in the tab the keyboard. The placeholder is never the keyboard card: ⌘[ ⌘] and Focus Follows Mouse skip it.
- Keys and ⌘K. In the floating window the menu bar and the palette offer only what applies to it: Detach Pane (⌘W, as the red light: a window's close puts the pane back, nothing stops), Redraw Terminal (⌥⌘R), Replace Pane (⇧⌘R), Open Terminal Here (on an agent, browser or plugin pane), Start Agent Here, Open Browser Here, Open File Explorer Here, Open Git Here (each but the pane's own kind), Close Pane (⇧⌘W), Stay on Top, Find, Find Next and Find Previous, the font size items, Copy and Paste, on a browser pane Reload Page (⌘R), Open Location (⌘L) and Show Web Inspector (⌥⌘I), and on a File or Markdown pane Save (⌘S) and on a Markdown pane Lock Editing (⌘E). The palette lists no tabs, spaces or sessions.
- Saved. The floating panes and their Stay on Top are kept with the main window that floated them, and open again when it restores. A pane that herdr closed closes its window; one that moved to another tab or space stays floating (its terminal is the same), and the main window stays where it is. Closing the main window closes its floating windows.
- The terminal streams the whole time it floats, whichever tab or session the main window shows. Another window on the same session shows the pane as a normal card.
- From other tools:
ovvi://float?session=‹herdr session›&pane=‹pane id›(and&machine=‹machine id›for a remote machine; This Mac otherwise) floats that pane over the machine's front main window, also when its session is not open there: then the session gets no page, no sidebar row and no page in view, and it disconnects when its last float goes (such a float does not come back after a relaunch). Open again for the same pane, it brings the window forward. A session or pane that is not there shows a notice and no window. With no window of a page yet (a URL that starts the app, or only the new-window view on screen), the window opens that session as its page, then floats the pane; a notification click does the same. - From a pane, on any machine:
ovvi float <pane>(CLI), run in a pane of a session the app follows, floats the pane it names the same way, andovvi unfloatdetaches it.
Notice
A short message after a failed action or a refused move, or from an agent.
- A capsule at the bottom center of the tab's page, 30 pt above its bottom, at least 36 pt
tall: glass, a leading symbol in a tone color (red
xmark.octagon.fillfor an error, orangeexclamationmark.triangle.fillfor a warning, blue for information, a spinner while a command runs, a greencheckmark.circle.fillwhen it ended), the text (two lines at most), an optional smaller second line (11 pt, secondary, two lines at most), an optional action button and ×. - Reconnecting (States): while a page reconnects over its cards, a notice with a spinner, "Reconnecting to ‹machine›…" and the reason as its second line, with no ×, under a notice and over the control capsule.
- A failed write's notice has no action: after a timeout the write may have happened, so a retry is not safe. A failed Relaunch with Chat's notice (Relaunch with Chat) has Retry, as has the Open Session panel's "Could not start" message.
- From an agent (Agent control):
"‹agent› opened ‹Kind› ‹title›" for a card that
ovvi openmade out of view, and "‹agent› points to ‹title›" forovvi revealof a pane out of view or a card thatovvi openfound already open. It is in the information tone, with Show, in each window that shows the session. Show selects the pane's space, tab and card in that window. Ovvi does not come to the front for it. - It goes after 4 s (a check after 2 s; one with Show or Retry after 6 s; a spinner when its command ends), or at ×. While the pointer is over it, it stays, and it goes when the pointer leaves if its time is up. A newer notice replaces it. After × or a newer notice, a command's check does not show; its error does. The next write ends an error notice (it may fix what failed); other notices keep their time.
Control capsule
Control is per session (D66). When two windows show the same session, the window where the user last typed or clicked in a terminal controls that session's panes, and the other window watches (Control between windows). Two windows on different sessions each control their own session.
- Its cards of that session draw their content at 0.85 of its opacity, and a
capsule at the bottom center of the tab's page, 30 pt above its bottom, says
"Controlled in another window", with Take Back. It is a
notice in the information tone with
macwindow.on.rectangleand no ×, sits under a notice when both show, and shows only while the page in view is watched. VoiceOver reads its text when it appears. - Take Back, or a key press, paste or click in one of its terminal cards, moves control of the session to this window. The other window then watches and shows the capsule.
- A pane that another device holds shows "In use elsewhere · Take Back" in its card instead (One controller per pane).
Command palette
⌘K or the title-bar ⌘ button opens it (D48). ⇧⌘P also opens it (Rex's key).
- 480 pt wide, with 10 pt around its content. The list is the exception at the bottom: it runs to the panel's bottom edge, so a row cut by the scroll is cut at that edge. Its 10 pt sit after the last row, and show only when the list is scrolled to its end. Open Session's list does the same with its 8 pt. The search field says "Search", in 15 pt text with 12 pt of padding at its sides and 9 pt above and below. It is a rounded rectangle of the panel's radius, 14 pt. Its fill is flat white at 8 % in dark and black at 6 % in light, in both styles: the panel is the same glass in both.
- Rows. 32 pt: a symbol, the title, and on the right the shortcut, or › for a row that opens a sub-page. The shortcut is one keycap per key (⇧, ⌘, W). The selected row is an accent pill, radius 8; the arrows or the pointer move it. Only a pointer that moves moves it: rows that scroll under a still pointer leave it where it is.
- Groups. On the first page with an empty field, the rows sit in groups, each under a header, in this order: Tabs, Spaces, Sessions, Panes, View, Edit and Plugins. A group holds the commands that act on that thing. Tabs lists the selected space's tabs first, then the tab commands. Spaces lists the spaces of every page in the window, with the session's name, then the space commands. Sessions lists the pages open in this window, then the session commands; Open Session… opens the Open Session panel. A group with no enabled command and no place does not show. A typed filter gives one list, best match first, and each row shows its group's name, dimmed, after its title. A sub-page shows only its own rows, under its title.
- Matching. Fuzzy, in the order of the typed letters, over titles and keywords: "split" finds Split Right, "dark" finds Appearance › by its keywords. On a tie, the title that is the whole filter comes first ("open file" gives Open File › before Open File Explorer Here), then a title that starts with the filter ("font size" gives Font Size › before Increase Font Size), then commands before places, then the menu's and herdr's order, pinned spaces first (the sidebar's in Manual).
- Rows at open. The palette reads its commands and places once, as it opens. A tab that changes its title while the palette is open keeps the old title in it, and a tab closed meanwhile stays listed and does nothing. Each typed key only filters these rows.
- Sub-pages. ⌫ on an empty field or Esc goes back one page; Esc on the
first page closes the palette.
- Rename Tab ›, Rename Space ›, Rename Pane ›: the field holds the current name (for a pane its label, empty when it has none); Enter saves.
- Move Pane to Tab ›: New Tab, New Space, then the session's other tabs under their spaces' names. A move into a tab lands right of its last card.
- Appearance › and Interface Style ›: a row highlighted for 0.75 s previews on the window. Enter keeps it; Esc restores the old one.
- Font Size ›: 8 to 32 pt.
- Start Agent Here ›: Claude Code, Codex, Pi and OpenCode (New pane).
- Open File ›: a field for a path.
~is the home folder, and a relative path starts at the keyboard card's folder, on its machine. Enter opens the file or folder with its default plugin, asovvi open ‹path›does, in a new tab; ⌥Enter opens it to the side. A path that names nothing gives a notice. - Open Commit ›: a field for a commit's hash or a ref (
main,HEAD~2). Enter opens Git at that commit of the keyboard card's repository, asovvi open git . ‹commit›does; ⌥Enter to the side.
- Open Working Tree opens Git at the uncommitted changes of the keyboard card's repository, in a new tab (⌥Enter: to the side).
- A typed path. On the first page, a filter that starts with
/,~,./or../and names a file or folder on the page's machine gets the row Open ‹path› first. Enter opens it as Open File does; ⌥Enter to the side. - Go to Directory (⇧⌘G, which is Find Previous over a File or Markdown
card) opens the system folder panel at the keyboard
card's folder. Choosing a folder types
cd -- '‹path›'and Return into the card. Enabled only when the card runs only its shell, and only on This Mac: the panel shows only this Mac's folders. - Insert Path (⇧⌘I) opens the same panel with files too. Choosing types the shell-quoted path at the cursor, without Return. On a remote machine the panel shows only This Mac's files: the chosen file is copied to the machine as a dropped file is (Drag and drop), and the copy's path is typed.
- Settings as commands. Each on/off setting in the menu bar is one command whose title says what it does: "Turn On Focus Follows Mouse" or "Turn Off Focus Follows Mouse".
The palette and the menu bar come from one command list, so each title and shortcut is written once (Keyboard shortcuts).
How it works: Layout and sizing, Zoom is the app's own, Platform: tab bar and Liquid Glass.