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 itsHostStore, 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'sLICENSE). 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 theherdrCLI andssh. The project turns Hardened Runtime on, but it applies only with a real signing identity, which notifications need too.
Dependencies
| Need | Choice | Why |
|---|---|---|
| Terminal drawing | Lakr233/libghostty-spm, exact pin (it brings DisplayLink) | Metal, IME, one Swift API |
| SSH | the system /usr/bin/ssh | the user's SSH setup works unchanged |
| The chat's Markdown | swiftlang/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 lockfile | recognized editors and viewers in the system's WebKit; no framework of our own |
| Building the plugins | Node 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 all | the 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
- One scroll monitor. One
NSEventmonitor 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). - 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.
- 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
ScrollViewin the hidden title bar. It is not aToolbarItem: the window has no toolbar, andTrafficLightsplaces 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 fordefault). - The tab bar follows Rex's (research notes in
resources/research/clients/rex.md). - One
Looktoken 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 typedev.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.
OvviUITestsdrives 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 localsshdon CI. Trackpad swipes and drags are posted as CGEvents byscripts/swipe.swiftandscripts/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, andFilesandGit(listings, reads, saves, the patch and the graph) run throughProcessExecand 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'sPluginManifestTests,PluginCallsTestsandPluginPackagesTestscover 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.