# byre for macOS Platform constraints, build requirements and SwiftUI behavior observed live. ## Decision **One web view only**: a task's Page view, which shows an HTML file from the task's worktree or one of its exposed previews in a `WKWebView`, in the attached sheet over the lead pane. Nothing else renders HTML: no `WKWebView` or `WebView` in the window, a dialog, Markdown, diagrams or the terminal. Any other link to a web page opens in the default browser. Mermaid needs a browser DOM, and its automatic layout can't give the banded, nested look of a hand-drawn architecture picture, so byre has its own ```` ```diagram ```` block (rows, nested boxes, a side column, routed arrows, named highlights), laid out on a grid by the app; its syntax is the `diagrams` knowledge-base article. It is byre's only diagram format: a ```` ```mermaid ```` block shows as code, and the workflow stage graphs are drawn by generating its source. Agents see their diagrams through the `render_diagram` tool, which runs the installed app's own binary as `Byre render-diagram` (source on stdin, PNG on stdout), so the picture is exactly what the transcript draws; that mode opens no window and never touches the app's iCloud-synced store, so it uses the default text size. byre's Markdown dialect (choice markers, `
` sections, `#/task/` links, worktree image paths, `NO_REPLY`) matches no library, so the app parses it itself. The app joins the tailnet as its own node with libtailscale and reaches byre's HTTPS origin on byre's node, so byre knows who is calling from the node itself. It never sends identity headers, and certificate validation is never relaxed. ## Stack Minimum macOS is the major version byre's host runs (`sw_vers --productVersion` prints it); there is no back-deployment. The project is a Swift package built with `swift build`, not an Xcode project. It builds with Xcode's toolchain: `xcode-select --print-path` must print `/Applications/Xcode.app/Contents/Developer`. With the Command Line Tools selected while Xcode is installed, Swift Build finds two toolchains for the Command Line Tools path and picks one at random on each run; the pick is part of the build request, so every change of pick rebuilds every target. `mac/bundle.sh` builds in one directory shared by every worktree, `~/Library/Caches/byre/build`, under a lock: SwiftPM keys a build on the package's absolute path, so a build in a fresh worktree recompiles every dependency. It copies sources in with `rsync --checksum`, which leaves unchanged files' modification times alone. It passes `--no-color-diagnostics` because `COLOR_DIAGNOSTICS` otherwise follows whether output is a terminal and is part of the build request, so agents whose shells differ would each rebuild every target in turn; `build-request.json` under `.build/out/Intermediates.noindex/XCBuildData/` shows what a build was keyed on. The server and helpers go in `Contents/Helpers`, not beside `Byre` in `Contents/MacOS`: the default APFS volume ignores case, so `byre` and `Byre` would be one file. Only the release configuration is built. A debug build stops at an `#error` in `ByreApp.swift`, and even a failed one leaves the next release build in the same build directory recompiling every target, since the two configurations share it. Release targets build whole-module, so a one-file change to `Byre` recompiles the whole target. `mac/bundle.sh` builds TailscaleKit from `github.com/tailscale/libtailscale` at the commit pinned in the script (`libtailscale=`), once per commit, and bundles it in `Contents/Frameworks` like Sparkle's. To move to a newer libtailscale, change the pinned commit. ## Signing Every build is signed for distribution outside the App Store: the team's Developer ID Application certificate, the hardened runtime and a secure timestamp, with the entitlements in `mac/byre.entitlements` and `mac/byre.provisionprofile` embedded. iCloud outside the App Store needs that profile: a Developer ID profile for the App ID `run.fc.byre.mac` with iCloud (CloudKit, container `iCloud.run.fc.byre.mac`) enabled, made at developer.apple.com under Certificates, Identifiers & Profiles → Profiles → Developer ID. It names no devices, so the app runs on any Mac. `security cms -D -i mac/byre.provisionprofile` prints it, and `security find-identity -v -p codesigning` lists the certificates on this Mac. Only the team's Account Holder can create a Developer ID Application certificate (Xcode → Settings → Accounts → Manage Certificates → + → Developer ID Application, signed in as the Account Holder); a Mac that builds the app needs that certificate and its private key in its keychain. A build that isn't notarized still runs on a Mac it was copied to with `client_upload`, since only downloaded files are quarantined. Notification permission needs the fixed bundle identifier `run.fc.byre.mac`. ## Startup and streams - Startup waits until the app's tailnet node lists the server's node among its peers, not just until it is `Running`: a node starting from libtailscale's netmap cache reports `Running` with no peers before control sends the real netmap, and a dial then waits out the SOCKS proxy's 5 s and is refused, which `URLSession` reports as "bad URL". - Run byre on this Mac and the Server tab run `byre install` through the user's login shell, since `byre install` copies its `PATH` into the service and the app's own `PATH` from launchd finds none of `claude`, `codex`, `agy` or `npx`. - SwiftUI's window restoration is off, but the scene keeps the id `main` that earlier builds used: AppKit restores their saved window by that id, and SwiftUI then opens no launch window at all when the id names no scene. ## Views - The transcript is a plain `VStack` in a `ScrollView`, not lazy: a `LazyVStack` estimates the height of rows it hasn't shown yet and corrects it when they scroll into view, without moving the scroll offset, so scrolling up through rows not yet shown jumped by each correction; SwiftUI can't move the offset mid-scroll without stopping its momentum. - The terminal cursor is a steady block: SwiftTerm's blinking cursor restarts its Core Animation fade on every keypress, scheduled 0.7 s out, and on a ProMotion display macOS then holds the window's screen update until that fade starts, so each typed letter appears about 0.7 s late. SwiftTerm redraws the whole screen whenever a terminal color is set, even when its value hasn't changed. - The Page view loads a worktree page from a plain-HTTP server the app runs on a loopback port (`http://127.0.0.1://`), which fetches each file through the server's `getTaskRaw` endpoint. The page's relative links to its stylesheets, scripts and images resolve to files beside it, and "↗ Browser" opens the same address. Previews load from their loopback forwards at `-.localhost`. App Transport Security blocks plain HTTP to `*.localhost` even with `NSAllowsLocalNetworking`; the `localhost` exception domain with subdomains in `Info.plist` allows it (loopback IP addresses need no exception). ## Updates The update UI must not offer Skip or turning automatic updates off: on a Mac running the server, `byre-update` would override either choice while the app isn't running. ## Persistence CloudKit has no unique constraints, so two Macs can each create a record for the same thing; duplicate records are expected. See `docs/operations.md`, "CloudKit schema", before changing the SwiftData models. The store is named `byre.store` because the app isn't sandboxed, so SwiftData's default `default.store` would be shared with every other unsandboxed app. Deleting the local files while the app isn't running makes the next launch download the records again. ## Gaps in SwiftUI Places where SwiftUI alone may fall short, each checked side by side against its fallback; the fallback is used only where the check failed: | Need | SwiftUI | Fallback | Spike result | | --- | --- | --- | --- | | Enter sends, Shift-Enter newline, nothing during IME composition | `TextEditor` with `onKeyPress` | `NSTextView` in an `NSViewRepresentable`, sending from `doCommand(by: insertNewline:)` | No fallback: `onKeyPress(.return)` runs before the `TextEditor`'s `NSTextView` and its input context. It sends, and returns `.ignored` for Shift so the text view adds the newline. Because it runs first, it must also return `.ignored` while the focused text view's `hasMarkedText()` is true, or Return sends the draft mid-composition. An in-process test that set marked text and delivered a Return `NSEvent` showed both: an unguarded handler sent the draft with the marked text still pending, and the guarded one passed Return on to the text view | | Image paste and drop into every box that talks to an agent | `onPasteCommand` and `onDrop` | The same `NSTextView`, overriding `paste(_:)` and drag handling | No fallback, but not through these two modifiers: `TextEditor`'s own text view takes ⌘V and drops before them and inserts the file's path as text. Drops land on a `Color.clear` overlay on the `TextEditor` with `onDrop` and `allowsHitTesting(false)`: it still receives the drag, and clicks and typing pass through to the editor (with hit testing on, the editor can't be clicked) | | Transcript of a long task scrolling smoothly and staying stuck to the bottom | `VStack` in a `ScrollView` | `List` with its own row reuse | No fallback: the stack stays at the bottom while rows arrive and a message streams, and stays put when scrolled up. A `LazyVStack` jumped while scrolling up (see "Views") | | Selecting text across blocks and messages | `textSelection(.enabled)` selects within one `Text` only | A selectable `NSTextView` in an `NSViewRepresentable` | Fallback used within a message: a drag can cross consecutive paragraphs, headings and lists. Other block types and message boundaries still interrupt selection | | Font from `mac/fonts/` | Core Text | Convert the files to TTF in the build script | No fallback: Core Text reads the woff2 files and their `wght` axis (200–800) | | SVG images from the worktree image endpoint | `NSImage(data:)` | Show the "image not available" text | No fallback: `NSImage(data:)` decodes SVG and draws it sharp at any size | Each family's latin and latin-ext files are subsets of one variable font with the same PostScript names, so registering both and looking the font up by name finds only the latin file. Use descriptors from the font files with latin-ext in the cascade to keep characters such as `ő` and `Ł` in Atkinson Hyperlegible.