Ovvi

Platform and code

The codebase, dependencies, swipes and views, the tab bar and tests

Project

  • One Xcode project, one macOS SwiftUI app target, and one local Swift package, HerdrKit.
  • Minimum OS: macOS 26 (Liquid Glass; one minimum means no fallbacks). Building needs Xcode 27.
  • Several windows (WindowGroup; File ▸ New Window, ⌘N). Each window shows one machine and has its own state: open pages and selection. Windows that show the same session share its HostStore, and control passes between them per session, so two windows never fight over a pane (D66). The other windows are the floating panes' (D50), each owned by a main window.
  • License: the GNU Affero General Public License v3.0 only (AGPL-3.0-only, the repository's LICENSE). The plugin API's types (@ovvi/plugin-api, web/ovvi) and the plugin template are MIT, so plugins can use any license. Every library the app or its default plugins bundle is under a license that goes with it (MIT, BSD, ISC, Apache-2.0, MPL-2.0, OFL, CC0, GPL-3.0 for a few syntax grammars). No GNU libintl: the libghostty package builds Ghostty with -Di18n=false.
  • Signing: ad hoc (make app), with App Sandbox off, because the app runs the herdr CLI and ssh. The project turns Hardened Runtime on, but it applies only with a real signing identity, which notifications need too.

Dependencies

NeedChoiceWhy
Terminal drawingLakr233/libghostty-spm, exact pin (it brings DisplayLink)Metal, IME, one Swift API
SSHthe system /usr/bin/sshthe user's SSH setup works unchanged
The chat's Markdownswiftlang/swift-markdown 0.9.0, exact pin (it brings cmark-gfm)Apple's GFM parser: tables, task lists, strikethrough (D86)
The default plugins' content (File Explorer and Git)CodeMirror 6 (editor, languages, search), @pierre/trees, markdown-it and pdfjs-dist in plugins/builtin/explorer, @pierre/diffs in plugins/builtin/git; exact pins in each package's lockfilerecognized editors and viewers in the system's WebKit; no framework of our own
Building the pluginsNode with npm (in the Brewfile, and installed on CI). Each package builds with its own esbuild and checks with its own TypeScript; web/ builds the API's runtime and checks it against its types. make web runs them allthe app ships no bundler and no package manager; at run time it uses the user's own (Build a plugin); no build output is in git

Each library has its entry, with its license, in the app's Acknowledgements.txt, the web ones included (Program icons).

Swipes and views

  1. One scroll monitor. One NSEvent monitor locks the axis at the start of each scroll gesture: a horizontal gesture over the sidebar pages its session pages; anything else goes to the view under the pointer. A gesture begun with ⌥ held goes to the window's sessions or the sidebar's rows (Swiping).
  2. SwiftUI destroys a tab's views when another tab shows. The window shows only the selected tab's page, a view of its own per tab, so a switch replaces it at once. The PaneViewRegistry owns every terminal and web view. Each SwiftUI wrapper makes its own container; a shown container takes the view. A hidden one (a kept page out of view) takes only a view no container has, and takes it as its page shows. A container removes the view only if it still owns it. Stream lifetime follows the HostStore, never whether a view is in a window. Off-screen surfaces stop drawing but keep reading their stream (herdr drops a watcher that reads nothing for 30 s). At most 25 streaming terminals and 25 web cards out of view (D46), least recently used first out; an evicted card frees its surface or its web view and keeps its last image.
  3. libghostty's default key bindings eat the app's shortcuts. Every surface gets keybind = clear, and keys go to the main menu first.

Tab bar and Liquid Glass

  • The tab bar is part of the app's own 46 pt title row, a horizontal ScrollView in the hidden title bar. It is not a ToolbarItem: the window has no toolbar, and TrafficLights places the buttons on the 46 pt row (an empty toolbar's accessibility frame covered the title row on a Mac with a notch, so a hit test at a tab found the toolbar).
  • One glass marker sits on the selected tab. A switch, a new tab and a close show at once, and the marker jumps with them: the tab bar has no motion of its own.
  • Each capsule has one status slot: × on the hovered tab, else the state icon of its strongest agent, a still SwiftUI shape (D42).
  • The window title (navigationTitle) is "‹tab title› - ‹page title›" (the machine's name for default).
  • The tab bar follows Rex's (research notes in resources/research/clients/rex.md).
  • One Look token value per style and appearance draws every surface (D44). Liquid Glass is only on small or short-lived parts: the Glass style's tab marker and sidebar pill, buttons, keycaps, the chat's reply box, popovers, the palette and floating panels. The window background is the classic behind-window blur (NSVisualEffectView, always active) under plain tints; the sidebar sits flat on the window, pane cards are translucent on it.

Other details

  • Drag and drop: the page holds the dragged item (Dragged: a pane, tab or space id). The pasteboard carries only the type dev.ovvi.app.drag, declared in the Info.plist, to route the drag to the app's drop targets. Tab capsules spring-load. During a drag, drop zones sit above each card's surface. Drops on another session or machine are refused.

Tests

  • End to end first. OvviUITests drives the app with XCUITest against a real herdr in throwaway sessions (ovvi-e2e-*), and checks the result through accessibility identifiers and through herdr itself (session.snapshot, pane.read). Remote tests go through a local sshd on CI. Trackpad swipes and drags are posted as CGEvents by scripts/swipe.swift and scripts/drag.swift.
  • Unit tests in HerdrKit (swift test), only for logic with branches: decoding a recorded herdr snapshot, the split-tree rebuild, the equal-split ratios, the snapshot pacing (one read in flight, read again), agent requests and their tokens, the seen rules, git status parsing, fuzzy matching, program icon matching, the terminal stream's frames, the remote command envelope and the relay, the agents' transcripts and closed sessions, session titles, pane labels, the plugin manifest, packages and calls, the SOCKS front, and Files and Git (listings, reads, saves, the patch and the graph) run through ProcessExec and the relay on a real repository.
  • Plugin tests (PluginTests, FileTests, MarkdownTests, FilesTests, GitTests, HeaderTests, PluginAPITests, PackageTests, PluginSettingsTests) open each page in a throwaway session and check the page's text and controls through accessibility, and the labels and files through herdr and the disk. They use their own config folder (-configFolder) and npm's offline cache, so they install and build plugins with no network. HerdrKit's PluginManifestTests, PluginCallsTests and PluginPackagesTests cover the manifest's rules, every call on a real repository and real files, and the sources and installs.

What it looks like: Main window, Theme and accessibility.

On this page