macOS client overlays, menus and notifications
This commit is contained in:
+11
-9
@@ -1,6 +1,6 @@
|
||||
# byre for macOS
|
||||
|
||||
This is a design, partly built. What exists under `mac/`: the generated API client, the SSE client, `Store` and `Feed`, the startup sequence, the connection screen with Change Server…, the generated palette, the window with its two panes and tab strips (More included), the Lead and task transcripts through `TranscriptBuilder`, the composer with drafts and images, the Markdown port, native flowchart diagrams, pane-focus reporting, the identity banner, and every other tab: Changes, Files, Terminal, Inbox, Reviews, Workflow (its stage graph drawn natively), Project, Status and Settings. Overlays so far are alerts, confirmations (used by Settings to delete a lead note), the More menu and the full-size image view. Settings asks for notification permission, but nothing delivers notifications until `Notifications.swift` exists. Until the switcher exists, the task picker is a plain menu of the active tasks, and the transcript head has Compact and Sync but not yet Merge or Discard. The rest of the work is under "M8 — macOS client" in `TODO.md`.
|
||||
This is a design, partly built. What exists under `mac/`: the generated API client, the SSE client, `Store` and `Feed`, the startup sequence, the connection screen with Change Server…, the generated palette, the window with its two panes and tab strips (More included), the Lead and task transcripts through `TranscriptBuilder`, the composer with drafts and images, the Markdown port, native flowchart diagrams, pane-focus reporting, the identity banner, every other tab (Changes, Files, Terminal, Inbox, Reviews, Workflow with its stage graph drawn natively, Project, Status and Settings), every overlay (the switcher, keyboard shortcuts, alerts, confirmations, the Merge and Discard dialogs, the More menu and the full-size image view), the Go menu with its Ctrl variants, and notifications. The rest of the work is under "M8 — macOS client" in `TODO.md`.
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -56,10 +56,10 @@ The Swift files follow the TypeScript ones, so a change in one points at the sam
|
||||
| `workflow.ts` | `Workflow.swift` | Stage graph and attempts |
|
||||
| `project.ts`, `todolink.ts` | `Project.swift`, `TodoLink.swift` | Project tab and TODO links |
|
||||
| `status.ts`, `settings.ts` | `Status.swift`, `Settings.swift` | Left-pane tabs |
|
||||
| `switcher.ts` | `Switcher.swift` | ⌘K overlay and the shortcuts help |
|
||||
| `switcher.ts` | `Switcher.swift` | ⌘K overlay, the shortcuts help and the Go actions (`runGo`) |
|
||||
| `drafts.ts` | `Drafts.swift` | Composer and answer drafts |
|
||||
| `notifications.ts` | `Notifications.swift` | `UNUserNotificationCenter` delivery |
|
||||
| `dom.ts` | `Dialogs.swift`, `Format.swift` | Overlays (alerts, confirmations, the More menu, the image view), `ago`, Copy, shared button and layout styles, diff lines |
|
||||
| `dom.ts` | `Dialogs.swift`, `Format.swift` | Overlays (alerts, modals with `showConfirm` and `showChoice`, the switcher's slot, the More menu, the image view) and the window's key monitor, `ago`, Copy, shared button and layout styles, diff lines |
|
||||
| `images.ts` | `Images.swift` | Blob, worktree and `data:` images, their cache and the full-size overlay |
|
||||
|
||||
## Store and events
|
||||
@@ -74,7 +74,7 @@ API calls go through the generated `Client` over `URLSessionTransport`. A middle
|
||||
|
||||
`EventStream` is a hand-written SSE client over `URLSession.bytes(for:)`. It splits lines itself, because `AsyncBytes.lines` drops the blank lines that end SSE events, and parses off the main actor, handing each frame to it. It handles `id`, `event: ping`, `event: ready` and `data`, and follows `subscribe` in `api.ts`: it tracks the last seq, reconnects from it after the same stale threshold (40 s, checked every 5 s) and retry delay (2 s), when the network comes back (`NWPathMonitor`), and when the app becomes active with a stale stream. An event whose data doesn't decode is logged and skipped. As in the web client there are two streams, global (after the bootstrap `last_event_seq`) and lead (`?lead=true`, after the last event of the lead's first page), and only the global one drives the connection dot.
|
||||
|
||||
Startup follows `main` in `app.ts`: `GET /api/admin/status`, then me, projects, inbox, lead sessions and summary reads in parallel, then drafts, focus, the lead stream, the window, the global stream. A failure before the window shows replaces it with `byre failed to start: <error>`, as the web page does; Change Server… leaves that screen. `handleEvent` doesn't call `notifyInbox` or `notifyTurn` until `Notifications.swift` exists.
|
||||
Startup follows `main` in `app.ts`: `GET /api/admin/status`, then me, projects, inbox, lead sessions and summary reads in parallel, then drafts, focus, the lead stream, the window, the global stream. A failure before the window shows replaces it with `byre failed to start: <error>`, as the web page does; Change Server… leaves that screen. `handleEvent` calls `notifyInbox` on `inbox.created` and `notifyTurn` on `turn.ended`, as `state.ts` does.
|
||||
|
||||
The app logs through `os.Logger` with subsystem `run.fc.byre.mac`, as the web client logs to the console: `log show --last 10m --predicate 'subsystem == "run.fc.byre.mac"'` prints it.
|
||||
|
||||
@@ -89,7 +89,9 @@ One window. The panes follow the "Web UI layout" rules in `AGENTS.md`: an `HStac
|
||||
|
||||
Overlays (the switcher, alerts, confirmations, the merge dialog, keyboard help) are SwiftUI views drawn above the panes inside the window, positioned as in the web client (the switcher and the Merge and Discard confirmations just below their anchor), not system sheets or alerts.
|
||||
|
||||
Closing the window leaves the app running with its store and streams, so notifications keep coming; clicking the Dock icon shows the window again. Quit flushes drafts and reports pane focus false before exiting.
|
||||
Overlays live in `Overlays` (`Dialogs.swift`), one environment object for the 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 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.
|
||||
|
||||
Closing the window leaves the app running with its store and streams (`applicationShouldTerminateAfterLastWindowClosed` returns false), so notifications keep coming; clicking the Dock icon shows the window again. Quit flushes drafts and reports pane focus false before exiting.
|
||||
|
||||
## Views
|
||||
|
||||
@@ -100,8 +102,8 @@ Each view does what its TypeScript counterpart does, with the same text, refresh
|
||||
- **Inbox.** The selected task's oldest open item, rendered from its Markdown context with each choice's marker replaced by a clickable block (pick-one, sends at once) or checkbox (pick-any, with Submit), the "recommended" tag, image-choice grids, attachments, Let the agent decide, Other… and, for errors, Reply… and Dismiss. Arrow keys move between choices. A 409 shows its error as in the browser. The Changes summary line sits above it.
|
||||
- **Changes, Files, Reviews, Workflow, Project, Status, Settings.** Ports of their files, including the pinned-snapshot stepper, open-diff memory, per-task place in Files, finding diffs with Fix and Ignore, nested subtasks, TODO Start and Discuss, rate-limit pace bars and lead notes. The Workflow tab draws its stage graph natively from the `WorkflowRun` (stages as boxes coloured by state, `after` as edges, laid out top to bottom by depth) with the layout and drawing code from "Diagrams", fed from the run's data rather than Mermaid text.
|
||||
- **Terminal.** A terminal view attached to `GET /api/tasks/{taskId}/terminal` with `URLSessionWebSocketTask`, following `terminal.ts`: binary input and output, `TerminalSize` JSON on resize, close code 4000 for `shell ended: <status>` with New shell, 1011 for a start failure, reconnect after the same delay while the task is active, one socket per open tab and selected task. Colours come from the palette as in the web client. Ctrl combinations typed in it go to the shell.
|
||||
- **Switcher.** The ⌘K overlay with the same ordering (tasks with inbox items first, then least recently visited), the `name: title` project prefix, New task, Start workflow… and its second step, projects, and Escape stepping back.
|
||||
- **Merge and Discard.** `confirmMerge` ported whole: the editable commit title, co-authors, warnings that refetch on the task's snapshot and review events and link to the Reviews or Inbox tab, and the parent-or-default-branch choice for subtasks.
|
||||
- **Switcher.** The ⌘K overlay, also opened by clicking the task picker, centred below the picker: the same ordering (tasks with inbox items first, then least recently visited, the selected task last), the `name: title` project prefix, New task, Start workflow… and its second step, projects, Escape stepping back, and the list scrolling to keep the highlighted row in view. Picking a task opens its Inbox when it has items, its unread summary when it has one, and its Transcript otherwise.
|
||||
- **Merge and Discard.** The transcript head's Merge button is enabled once `GET /api/tasks/{taskId}/unmerged` reports commits or files, refetched 1.5 s after a tool result or turn end, and labelled and disabled as in `taskHead`. `confirmMerge` (`MergeDialog.swift`) is ported whole: the editable commit title (at most 72 characters, kept across a warning link's detour), co-authors, warnings that refetch on the task's snapshot and review events and link to the Reviews or Inbox tab, the parent-or-default-branch choice for subtasks, and Merge now / After review while the latest snapshot's review is running. It sits just below the Merge button, or centred when ⌘S opens it with the button off screen. Discard confirms below its button, then clears the focused task.
|
||||
|
||||
## Markdown
|
||||
|
||||
@@ -135,7 +137,7 @@ Images load through the API: `GET /api/blobs/{id}`, `GET /api/tasks/{taskId}/ima
|
||||
|
||||
## Notifications
|
||||
|
||||
`Notifications.swift` ports `notifications.ts` onto `UNUserNotificationCenter`: the same preferences from `GET /api/me`, the same events (`inbox.created` by kind, `turn.ended` with `summary: true`), title `byre · <task title>`, body cut at 200 characters, and the item id or `turn-<seq>` as the request identifier. Clicking one shows the window and focuses the task. Settings shows the authorization state and a button that requests it, in place of the browser permission flow. As in the browser, nothing notifies while the app isn't running.
|
||||
`Notifications.swift` ports `notifications.ts` onto `UNUserNotificationCenter`: the same preferences from `GET /api/me`, the same events (`inbox.created` by kind, `turn.ended` with `summary: true`), title `byre · <task title>`, body the first line cut at 200 characters, and the item id or `turn-<seq>` as the request identifier, so a repeat replaces the earlier one as the browser's `tag` does. They show as banners even while byre is the active app, as browser notifications do over the page (`Notifier`, the centre's delegate, set at launch). Clicking one activates the app, shows the window (reopening it if it was closed) and focuses the task's Transcript. A notification left in Notification Center after quitting relaunches the app when clicked; `Notifier` holds its task until startup has a store, then focuses it. Settings shows the authorization state and a button that requests it, in place of the browser permission flow. As in the browser, nothing notifies while the app isn't running.
|
||||
|
||||
## Pane focus
|
||||
|
||||
@@ -147,7 +149,7 @@ Images load through the API: `GET /api/blobs/{id}`, `GET /api/tasks/{taskId}/ima
|
||||
|
||||
## Keyboard and menus
|
||||
|
||||
The app menu has About, Change Server…, Hide, Hide Others and Quit; 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 and Keyboard Shortcuts ⌘/; View has Enter Full Screen; Window has the standard items. There is no File menu. The Go commands do nothing while an overlay is open, as `bindShortcuts` ignores them while a dialog is open. The Ctrl variants the web client also accepts work the same way through a local key monitor, except while the terminal has focus, where Ctrl combinations go to the shell. Escape closes the open overlay.
|
||||
The app menu has About, Change Server…, Hide, Hide Others and Quit; 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 and Keyboard Shortcuts ⌘/; View has Enter Full Screen; Window has the standard items. There is no File menu. The Go commands are disabled while an overlay is open, as `bindShortcuts` ignores them while a dialog is open. The Ctrl variants the web client also accepts (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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user