Replace server tabs with one window and a server picker
This commit is contained in:
+8
-8
@@ -41,7 +41,7 @@ SwiftUI's own macros don't build here: the SDK declares `@State`, `@Entry`, `@An
|
||||
| --- | --- |
|
||||
| `API.swift`, `EventStream.swift` | The generated client over `URLSession`; the SSE client |
|
||||
| `Store.swift`, `Feed.swift` | The one `@Observable` store, `handleEvent`, per-task feeds, focus |
|
||||
| `ByreApp.swift`, `Startup.swift` | The `App`, menus, the saved servers and their connections (`Servers`, `Connection`), server windows, the connection screen, startup sequence |
|
||||
| `ByreApp.swift`, `Startup.swift` | The `App`, menus, the saved servers and their connections (`Servers`, `Connection`), the window and its server picker, the connection screen, startup sequence |
|
||||
| `Panes.swift`, `PaneFocus.swift` | The two panes, tab strips, More menu, pane-focus reporting |
|
||||
| `Chat.swift`, `TranscriptBuilder.swift`, `Composer.swift`, `MergeDialog.swift` | Transcript and composer for tasks and the lead |
|
||||
| `Markdown.swift`, `MarkdownView.swift` | byre's Markdown parser to a block tree, and its views |
|
||||
@@ -82,18 +82,18 @@ Focus lives in `Store`. In-app `#/` links go through `Store.route`: `#/task/<id>
|
||||
|
||||
## Window and layout
|
||||
|
||||
One window per saved server, sharing no UI: the windows are a `WindowGroup` (id `main`) whose value is the server's origin, so opening a server's window again brings forward the one already open. The windows are tabs of one window: each sets `tabbingMode = .preferred` and, once shown, joins the tab group of another visible server window (`addTabbedWindow`), so the windows opened at launch end up tabbed together as New Server Window's do. `addTabbedWindow` puts the new tab right after the host's, so the order would follow which windows came up first; instead, each time a window joins or its server changes, `Servers.arrange` moves the tabs with a server into saved order (`ServerRecord.saved`, the order Previous and Next Server follow) with `NSWindowTabGroup.insertWindow`, then restores the selected tab, which `insertWindow` changes. Tabs with no server stay after them. Each window has its own `Overlays`, so its switcher, dialogs, left-pane tab and key monitor act only on it, and its own pane-focus reporting. The window's title is the server's host, so the Window menu lists the servers, and its content sits inside a 3-point border in the server's colour (`Servers.color`, one of the task colours, picked from a row of swatches in Settings under Window colour and kept in the server's `ServerRecord`, so every Mac shows the same colour). A window with no server (New Server Window, or after Forget Server) shows the connection screen with no border. The panes follow the "UI layout" rules in `AGENTS.md`: an `HStack` of two panes of exactly equal width at every size, no sidebar, no navbar, nothing above them but the identity banner when the user has no git identity. The title bar is the standard one; no byre controls go in it or in a toolbar.
|
||||
One window (a `Window` scene, id `main`) shows one saved server at a time: `Servers.current`, the origin it shows, or nil for the connection screen. The title bar holds no title, only the server picker (`ServerPicker`, a toolbar item in the principal placement): the current server's colour dot and short name (its host up to the first dot, `serverName`), or Add Server with none. Its menu lists every saved server in saved order (`ServerRecord.saved`, the order Previous and Next Server follow), then Add Server…, which shows the connection screen. Switching servers swaps the window's content; every connection keeps running in the background. Each server has its own `Overlays`, held by its `Connection`, so its switcher, dialogs and left-pane tab survive switching away and back, and its `MainView` is rebuilt (`.id(origin)`) so no view state carries from one server to another. The content sits inside a 3-point border in the server's colour (`Servers.color`, one of the task colours, picked from a row of swatches in Settings under Window colour and kept in the server's `ServerRecord`, so every Mac shows the same colour). The connection screen has no border. The panes follow the "UI layout" rules in `AGENTS.md`: an `HStack` of two panes of exactly equal width at every size, no sidebar, no navbar, nothing above them but the identity banner when the user has no git identity.
|
||||
|
||||
At launch the first window takes the oldest saved server and opens a window for each of the others. A CloudKit import that brings a server saved on another Mac starts its connection, and a window showing an empty connection screen takes it. SwiftUI's own window restoration is off (`restorationBehavior(.disabled)`); the scene keeps the id `main` that the single-window builds used, because AppKit restores their saved window by that id and SwiftUI then opens no launch window at all when the id names no scene.
|
||||
At launch the window shows the oldest saved server. A CloudKit import that brings a server saved on another Mac starts its connection, and the window takes it while it shows an empty connection screen. A CloudKit import that removes the current server switches to the first saved one. SwiftUI's own window restoration is off (`restorationBehavior(.disabled)`); the scene keeps the id `main` that earlier builds used, because AppKit restores their saved window by that id and SwiftUI then opens no launch window at all when the id names no scene.
|
||||
|
||||
- **Left pane:** tabs Lead, Status, Server, Settings, with "reconnecting…", the connection dot and the user's name at the end of the strip.
|
||||
- **Right pane:** the task bar (task picker `<project> ▸ <title> ▾` and the preview links), then the task's tabs in the server's focus order, hiding Questions, Reviews and Workflow when they have nothing, with a **More ▾** menu for tabs that don't fit, carrying their badges. The pane's accent is the selected task's colour.
|
||||
|
||||
Overlays (the switcher, alerts, confirmations, the merge dialog, keyboard help) are SwiftUI views drawn above the panes inside the window, not system sheets or alerts. The switcher and the Discard confirmation sit just below their anchor; the merge dialog is centred in the right pane.
|
||||
|
||||
Overlays live in `Overlays` (`Dialogs.swift`), one environment object for each window. An open overlay takes Escape (closing the topmost: alert, modal, image, switcher step or switcher, More menu) and Enter (the alert's OK, the modal's last button when its `canSubmit` holds, the switcher's highlighted row) through a local key monitor, which handles only key events for its own window and passes Enter on while an input method holds marked text. The switcher also takes ↑ and ↓ there. The left pane's selected tab lives in `Overlays` too, so ⌘L can switch to Lead and focus its composer.
|
||||
Overlays live in `Overlays` (`Dialogs.swift`), one environment object for each server. An open overlay takes Escape (closing the topmost: alert, modal, image, switcher step or switcher, More menu) and Enter (the alert's OK, the modal's last button when its `canSubmit` holds, the switcher's highlighted row) through a local key monitor, which handles only key events for its own window and passes Enter on while an input method holds marked text. The switcher also takes ↑ and ↓ there. The left pane's selected tab lives in `Overlays` too, so ⌘L can switch to Lead and focus its composer.
|
||||
|
||||
Connections belong to `Servers`, not to windows: closing a server's window leaves its store and streams running (and the app running after the last window, `applicationShouldTerminateAfterLastWindowClosed` returns false), so its notifications keep coming. Clicking the Dock icon with no window showing opens every saved server's window again (`applicationShouldHandleReopen`), and File → Open Server opens one. Quit flushes every server's drafts and reports pane focus false before exiting.
|
||||
Connections belong to `Servers`, not to the window: every saved server's store and streams run whichever one the window shows, and closing the window leaves them all running (and the app running, `applicationShouldTerminateAfterLastWindowClosed` returns false), so notifications keep coming. Clicking the Dock icon with no window showing opens it again (`applicationShouldHandleReopen`). Quit flushes every server's drafts and reports pane focus false before exiting.
|
||||
|
||||
## Views
|
||||
|
||||
@@ -137,7 +137,7 @@ Images load through the API: `GET /api/blobs/{id}`, `GET /api/tasks/{taskId}/ima
|
||||
|
||||
## Notifications
|
||||
|
||||
`Notifications.swift` posts through `UNUserNotificationCenter`: the preferences from `GET /api/me`, the events (`inbox.created` by kind, `turn.ended` with `summary: true`), title `byre · <task title>`, body the first line cut at 200 characters, `task_id` and `origin` (the server's) in its user info, and the item id, `turn-<seq>@<origin>` or `error-<seq>@<origin>` as the request identifier, so a repeat replaces the earlier one and two servers' event seqs don't collide. Every connected server notifies, its window open or not. They show as banners even while byre is the active app (`Notifier`, the centre's delegate, set at launch). Clicking one activates the app, opens or brings forward its server's window and focuses the task's Transcript. A notification left in Notification Center after quitting relaunches the app when clicked; `Notifier` holds its server and task until that server's startup has a store, then focuses it. Settings shows the authorization state (`This Mac shows notifications while byre is running.`, `This Mac doesn't show notifications from byre.` with a **Turn on for this Mac** button, which asks macOS for permission, or `Notifications are blocked for byre in System Settings.`). Nothing notifies while the app isn't running.
|
||||
`Notifications.swift` posts through `UNUserNotificationCenter`: the preferences from `GET /api/me`, the events (`inbox.created` by kind, `turn.ended` with `summary: true`), title `byre · <task title>`, body the first line cut at 200 characters, `task_id` and `origin` (the server's) in its user info, and the item id, `turn-<seq>@<origin>` or `error-<seq>@<origin>` as the request identifier, so a repeat replaces the earlier one and two servers' event seqs don't collide. Every connected server notifies, its window open or not. They show as banners even while byre is the active app (`Notifier`, the centre's delegate, set at launch). Clicking one activates the app, opens the window on its server and focuses the task's Transcript. A notification left in Notification Center after quitting relaunches the app when clicked; `Notifier` holds its server and task until that server's startup has a store, then focuses it. Settings shows the authorization state (`This Mac shows notifications while byre is running.`, `This Mac doesn't show notifications from byre.` with a **Turn on for this Mac** button, which asks macOS for permission, or `Notifications are blocked for byre in System Settings.`). Nothing notifies while the app isn't running.
|
||||
|
||||
## Pane focus
|
||||
|
||||
@@ -149,11 +149,11 @@ Images load through the API: `GET /api/blobs/{id}`, `GET /api/tasks/{taskId}/ima
|
||||
|
||||
## Keyboard and menus
|
||||
|
||||
The app menu has About, Check for Updates…, Forget Server (for the focused window's server), Hide, Hide Others and Quit; File has New Server Window ⌘N, which opens a window with the connection screen, and Open Server, listing every saved server by host; Edit has the standard items, with Cut, Copy, Paste and Select All being the app's own commands so Paste can attach images (see "Gaps in SwiftUI"); a **Go** menu has Switch Task ⌘K, Project ⌘P, Lead ⌘L, Merge ⌘S, Previous Server ⌘[, Next Server ⌘] and Keyboard Shortcuts ⌘/; View has Enter Full Screen; Window has the standard items and one entry per open window. The Go commands act on the focused window (its `ServerFocus`, a focused scene value holding its store and `Overlays`), and are disabled while that window's alert or modal is open (`dialogOpen`); the switcher and the More menu don't block them, and ⌘L closes the switcher so the Lead composer gets the keys. Previous and Next Server bring forward, or open, the window of the saved server before or after the focused window's, in saved order, wrapping round; from a window with no server it goes to the first. The Ctrl variants (Ctrl-K, P, L, S and /) run the same actions from the key monitor in `Overlays`, except while the terminal's `TerminalView` is the first responder (`terminalFocused`), where they go to the shell. ⌘ combinations always reach the menu, the terminal included. Escape closes the open overlay.
|
||||
The app menu has About, Check for Updates…, Forget Server (for the server the window shows), Hide, Hide Others and Quit; File has Add Server… ⌘N, which shows the connection screen; Edit has the standard items, with Cut, Copy, Paste and Select All being the app's own commands so Paste can attach images (see "Gaps in SwiftUI"); a **Go** menu has Switch Task ⌘K, Project ⌘P, Lead ⌘L, Merge ⌘S, Previous Server ⌘[, Next Server ⌘] and Keyboard Shortcuts ⌘/; View has Enter Full Screen; Window has the standard items and the window. The Go commands act on the server the window shows (its `ServerFocus`, a focused scene value holding its store and `Overlays`), and are disabled while that server's alert or modal is open (`dialogOpen`); the switcher and the More menu don't block them, and ⌘L closes the switcher so the Lead composer gets the keys. Previous and Next Server switch the window to the saved server before or after the current one, in saved order, wrapping round; from the connection screen they go to the first. The Ctrl variants (Ctrl-K, P, L, S and /) run the same actions from the key monitor in `Overlays`, except while the terminal's `TerminalView` is the first responder (`terminalFocused`), where they go to the shell. ⌘ combinations always reach the menu, the terminal included. Escape closes the open overlay.
|
||||
|
||||
## Connection
|
||||
|
||||
On first launch, and in a window from New Server Window, the window shows one field, Server URL (an `https` origin with no path), Connect, and Run byre on this Mac. Run byre on this Mac runs the bundle's `Contents/Helpers/byre install` through the user's login shell (`pw_shell -l -c 'exec "$BYRE_HELPER" install'`, which sh, zsh, bash and fish all accept), 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`; it takes the address from the `byre is at` line, polls `GET /api/admin/status` for up to 10 s while the service starts, then connects to it as Connect does. Connect calls `GET /api/admin/status` and explains a failure: an untrusted certificate, an unreachable server ("Is Tailscale connected?"), 401 (no Tailscale identity: use the `ts.net` HTTPS address), 403 (the request didn't come through `tailscale serve`); anything else shows the error's own text. Certificate validation is never relaxed. On success the origin, normalized to a lower-case host with any port, is saved as a `ServerRecord` (see "Persistence") with the first task colour no other saved server uses, startup runs, and the window becomes that server's; later launches start against every saved server directly. Connecting to a server whose startup failed replaces its failed connection with a new one, keeping its record and colour. Connecting to a server that already has a window brings that window forward and closes this one, so each server has one window and one `Store`; worktree images (`ImageCache`) and review patches (keyed by origin) are cached per server. Each finished CloudKit import (`NSPersistentCloudKitContainer.eventChangedNotification`) reloads the saved servers: it starts a connection for each new one and stops the connection of each one forgotten on another Mac, and a window showing the connection screen with an empty field takes the first new one, so on a new Mac the address another Mac saved connects without typing. Forget Server deletes the window's `ServerRecord` (on every Mac, through iCloud), stops its connection and shows the connection screen in that window with the old origin in the field.
|
||||
On first launch, and from Add Server…, the window shows one field, Server URL (an `https` origin with no path), Connect, and Run byre on this Mac. Run byre on this Mac runs the bundle's `Contents/Helpers/byre install` through the user's login shell (`pw_shell -l -c 'exec "$BYRE_HELPER" install'`, which sh, zsh, bash and fish all accept), 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`; it takes the address from the `byre is at` line, polls `GET /api/admin/status` for up to 10 s while the service starts, then connects to it as Connect does. Connect calls `GET /api/admin/status` and explains a failure: an untrusted certificate, an unreachable server ("Is Tailscale connected?"), 401 (no Tailscale identity: use the `ts.net` HTTPS address), 403 (the request didn't come through `tailscale serve`); anything else shows the error's own text. Certificate validation is never relaxed. On success the origin, normalized to a lower-case host with any port, is saved as a `ServerRecord` (see "Persistence") with the first task colour no other saved server uses, startup runs, and the window switches to that server; later launches start against every saved server directly. Connecting to a server whose startup failed replaces its failed connection with a new one, keeping its record and colour. Connecting to a saved server switches to it, so each server has one `Store`; worktree images (`ImageCache`) and review patches (keyed by origin) are cached per server. Each finished CloudKit import (`NSPersistentCloudKitContainer.eventChangedNotification`) reloads the saved servers: it starts a connection for each new one and stops the connection of each one forgotten on another Mac, and when the window shows the connection screen with an empty field it takes the first new one, so on a new Mac the address another Mac saved connects without typing. Forget Server deletes the current server's `ServerRecord` (on every Mac, through iCloud), stops its connection and shows the connection screen with the old origin in the field.
|
||||
|
||||
## Updates
|
||||
|
||||
|
||||
+3
-3
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user