Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ce87ef704 | ||
|
|
e7e6fae758 | ||
|
|
36f4955a09 | ||
|
|
613640fab4 | ||
|
|
57d2b25ce9 | ||
|
|
6ac297d374 | ||
|
|
b72b4eda7f | ||
|
|
52a231e635 | ||
|
|
88d1146778 |
@@ -15,6 +15,7 @@ A deploy is a release that isn't uploaded: the same notarized disk image, instal
|
||||
1. Run `mac/deploy.sh`, always as the last action of the turn. It runs `mac/package.sh` (the build, both notarizations and the disk image, as for a release, into `mac/.build/release/<version>/`), and installs the image's app as `/Applications/Byre.app` with `mac/install.sh`. The service runs `/Applications/Byre.app/Contents/Helpers/byre`; within 5 seconds it sees that file replaced and exits, and launchd starts the new server and `byre-hands`. Everything is signed with Developer ID, so the macOS privacy grants (Screen Recording, Accessibility, Automation) survive.
|
||||
2. When you resume, confirm it came up: read the tail of `~/Library/Logs/byre.log`, and `GET /api/admin/status` through the `tailscale serve` URL (`tailscale serve status` shows it). Report what you deployed and what you checked.
|
||||
3. Once the service is confirmed healthy, update each client Mac whose app is running, through the client tools and without asking first: upload the disk image (`mac/.build/release/<version>/byre-<version>.dmg`) and `mac/install.sh`, quit the app, run `install.sh <dmg>`, and `open /Applications/Byre.app`. The app reads its server address from its iCloud-synced SwiftData store, so it reconnects to prod with no setup. Report each Mac's app commit (`install.sh` prints it).
|
||||
4. End the report with release notes for everything on main since the last release tag (`git describe --tags --abbrev=0` names it), written for a byre user in the "Releasing" shape (`## New` and `## Fixed`, each a bullet with a bold short name and what it does or what now works; no `## Install`). Read `git diff <tag>..HEAD`, not just the commit subjects, and leave out changes no user sees.
|
||||
|
||||
What to expect:
|
||||
|
||||
|
||||
+6
-2
@@ -1215,13 +1215,17 @@ components:
|
||||
|
||||
NotificationPrefs:
|
||||
type: object
|
||||
description: Which new questions, finished turns, errors and rate limits notify this user in their open byre pages.
|
||||
required: [questions, reports, errors, rate_limits]
|
||||
description: Which task events notify this user in the macOS app.
|
||||
required: [questions, reports, errors, rate_limits, ready, discard, merge_problems, workflows]
|
||||
properties:
|
||||
questions: { type: boolean, description: Questions. On by default. }
|
||||
reports: { type: boolean, description: "Task turns that end with a summary (turn.ended with summary: true)." }
|
||||
errors: { type: boolean, description: session.error events on tasks. }
|
||||
rate_limits: { type: boolean, description: session.rate_limited events on tasks. }
|
||||
ready: { type: boolean, description: task.ready events. }
|
||||
discard: { type: boolean, description: task.discard_suggested events. }
|
||||
merge_problems: { type: boolean, description: task.merge_stopped and task.merge_after_review_dropped events. }
|
||||
workflows: { type: boolean, description: task.workflow_finished events. }
|
||||
|
||||
UserUpdate:
|
||||
type: object
|
||||
|
||||
+2
-2
@@ -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`: it tracks the last seq, reconnects from it after a stale threshold (40 s, checked every 5 s) and a 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. 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: `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>` and the whole startup is tried again every 2 s until it succeeds, so a server restarting (after an update replaces its binary, for one) heals by itself; Forget Server leaves that screen. `handleEvent` calls `notifyInbox` on `inbox.created` and `notifyTurn` on `turn.ended`.
|
||||
Startup: `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>` and the whole startup is tried again every 2 s until it succeeds, so a server restarting (after an update replaces its binary, for one) heals by itself; Forget Server leaves that screen. `handleEvent` calls `notifyInbox` on `inbox.created`, `notifyTurn` on `turn.ended`, and `notifyTask` on the task events listed under "Notifications" in `docs/operations.md`.
|
||||
|
||||
The app logs through `os.Logger` with subsystem `run.fc.byre.mac`: `log show --last 10m --predicate 'subsystem == "run.fc.byre.mac"'` prints it.
|
||||
|
||||
@@ -157,7 +157,7 @@ On first launch, and from Add Server…, the window shows one field, Server URL
|
||||
|
||||
## Updates
|
||||
|
||||
`Updater.swift` runs a Sparkle `SPUUpdater`, started at launch, with `Updater` as its user driver and delegate instead of Sparkle's standard windows, so no update UI offers Skip or turning automatic updates off, which `byre-update` would override. It reads `SUFeedURL` from `mac/Info.plist`, the appcast of the newest release (see "Releases" in `docs/operations.md`), checks it daily (`SUEnableAutomaticChecks`, so Sparkle doesn't first ask whether to), compares the appcast's `sparkle:version` with `CFBundleVersion`, and verifies the download's EdDSA signature against `SUPublicEDKey` and its Developer ID signature against the running app's. `SUAutomaticallyUpdate` makes a scheduled check download a new release in the background and stage it to install when the app quits; Sparkle then calls the delegate's `willInstallUpdateOnQuit`, which shows an alert, "Byre <version> is available", with **Update Now** (Sparkle installs and relaunches the app) and **Update on Quit** (it stays staged), and returns true, so Sparkle shows nothing of its own later. Returning true also holds Sparkle's update session open until the app quits, so no further checks run; `Updater` keeps the staged item and Sparkle's install handler, and Check for Updates… then shows the alert for that update again instead of checking. Otherwise Check for Updates… in the app menu checks at once: an update found shows the same alert (`showUpdateFound`), and either answer downloads it; at `showReadyToInstallAndRelaunch` the driver answers Install for Update Now and Dismiss for Update on Quit, which leaves it to install on quit. No update, or an error, shows an alert with Sparkle's message. The alerts are `NSAlert.runModal`; there is no download progress UI, and the appcast carries no release notes. When the bundle is replaced, a server running from its `Contents/Helpers/byre` restarts itself onto the new binary (see "Running as a service" in `docs/operations.md`).
|
||||
`Updater.swift` runs a Sparkle `SPUUpdater`, started at launch, with `Updater` as its user driver and delegate instead of Sparkle's standard windows, so no update UI offers Skip or turning automatic updates off, which `byre-update` would override. It reads `SUFeedURL` from `mac/Info.plist`, the appcast of the newest release (see "Releases" in `docs/operations.md`), checks it daily (`SUEnableAutomaticChecks`, so Sparkle doesn't first ask whether to), compares the appcast's `sparkle:version` with `CFBundleVersion`, and verifies the download's EdDSA signature against `SUPublicEDKey` and its Developer ID signature against the running app's. `SUAutomaticallyUpdate` makes a scheduled check download a new release in the background and stage it to install when the app quits; Sparkle then calls the delegate's `willInstallUpdateOnQuit`, which shows an alert, "Byre <version> is available", with **Update Now** (Sparkle installs and relaunches the app) and **Update on Quit** (it stays staged), and returns true, so Sparkle shows nothing of its own later. Returning true also holds Sparkle's update session open until the app quits, so no further checks run; `Updater` keeps the staged item and Sparkle's install handler, and Check for Updates… then shows the alert for that update again instead of checking. Otherwise Check for Updates… in the app menu checks at once: an update found shows the same alert (`showUpdateFound`), and either answer downloads it; at `showReadyToInstallAndRelaunch` the driver answers Install for Update Now and Dismiss for Update on Quit, which leaves it to install on quit. No update, or an error, shows an alert with Sparkle's message. The alerts are `NSAlert.runModal`, and the appcast carries no release notes. After Update Now, an `NSPanel` hosting `UpdateProgressView` shows each phase on its own bar starting from zero, with no combined percentage: Downloading (bytes received of the `Content-Length`, with Cancel, which calls Sparkle's cancellation), Extracting (Sparkle's extraction progress) and Installing (indeterminate, until the app relaunches). An Update on Quit download shows no panel. Sparkle's `dismissUpdateInstallation`, an error, or the relaunch closes it. When the bundle is replaced, a server running from its `Contents/Helpers/byre` restarts itself onto the new binary (see "Running as a service" in `docs/operations.md`).
|
||||
|
||||
A Mac that runs the server but not the app is updated by `byre-update` (`mac/Sources/byre-update/main.swift`, in `Contents/Helpers`), which the server runs daily. It drives the same Sparkle updater with no UI: an `SPUUpdater` whose host and application bundle are the app it sits in, with a user driver that answers Install to every offer and logs to stderr instead of showing windows. It makes one check (`checkForUpdates`, which, as a user-initiated check, would ignore a version skipped in the app), and exits when Sparkle reports no update, an error, or the end of the update session. Sparkle's installer replaces the bundle, and since the app isn't running it doesn't relaunch it. When that app bundle is running (a `NSWorkspace` running application with that `bundleURL`), it does nothing and exits, leaving the update to the app's own updater and its alert. Both use the app's user defaults (`run.fc.byre.mac`), so a check by either sets Sparkle's last-check time for both.
|
||||
|
||||
|
||||
+8
-7
@@ -37,7 +37,7 @@ A task ends `merged` or `discarded`; either way its sessions are stopped, its sn
|
||||
|
||||
A task's summary is what its agent writes, before the task closes, about the whole task: what it set out to do, what was built or tried, the key decisions and why, and what was left out. The agent sets it with the `set_task_summary` MCP tool (`internal/agents/summary.go`), any time while the task is active; each call replaces `tasks.summary` and records a `task.summary` event carrying it, which the transcript shows as a note. The Merge turn's prompt and the discard turn's prompt (`summaryRequest`) both ask for it. Turn summaries don't stand in for it: each covers only its own turn, and a merged task's last one is always the Merge turn's. Tasks closed before the column existed have none. `sqlite3 byre.db "SELECT id, status, summary FROM tasks WHERE status != 'active' ORDER BY closed_at DESC LIMIT 5"` shows recent ones.
|
||||
|
||||
An implementer says its task is ready to merge with the `ready_for_merge` MCP tool (`internal/agents/ready.go`), whose `overview` covers the whole task for a human switching between tasks: a summary, tradeoffs and compromises, what was not done, and future TODOs. Its description tells the agent to call it only once the review of its most recent changes has come back, never in the same turn as a change or while that review is pending. Each call records a `task.ready` event carrying it, which the lead gets as a signal and the transcript draws as a bubble (`ReadyBubble` in `mac/Sources/Byre/Chat.swift`) headed "ready for merge". The latest one, while the task is active and neither a question nor a snapshot other than an answer snapshot has come since it (a question, or newer work the overview doesn't cover), is drawn like an open question (the warn tint and border) and carries small Merge and Discard buttons that act like the header's, confirmation dialogs included; earlier ones are drawn like an answered question (the faint warn tint), without buttons. Recording it changes nothing else. byre asks for it (`readyPrompt`, sent as a byre message) when a task's state goes to `idle` (`Manager.Observe` calls `promptReady`) and the latest non-answer snapshot's review is `done` or `skipped`, once per snapshot. It checks and sends under the session's delivery lock (the one human messages and answers take), and only to an agent that isn't running, so the prompt never queues behind a turn that could add unreviewed work; `tasks.ready_prompt_n` holds the last snapshot asked about, set only once the prompt's turn started, so turns that change no files don't ask again and a new snapshot does. It doesn't ask for workflow stage tasks, a task with a merge after review set, or a review that failed, and it skips the `reviewing` → `idle` change of a review with findings, since those go to the agent; the turn that answers them asks once it ends without new work. The prompt tells the agent not to call the tool, and to say what's left, when the task isn't ready.
|
||||
An implementer says its task is ready to merge with the `ready_for_merge` MCP tool (`internal/agents/ready.go`), whose `overview` covers the whole task for a human switching between tasks: a summary, tradeoffs and compromises, what was not done, and future TODOs. Its description tells the agent to call it only once the review of its most recent changes has come back, never in the same turn as a change or while that review is pending. Each call records a `task.ready` event carrying it, which the lead gets as a signal and the transcript draws as a bubble (`ReadyBubble` in `mac/Sources/Byre/Chat.swift`) headed "ready for merge". The latest one, while the task is active and neither a question nor a snapshot other than an answer snapshot has come since it (a question, or newer work the overview doesn't cover), is drawn like an open question (the warn tint and border) and carries small Merge and Discard buttons that act like the header's, confirmation dialogs included; earlier ones are drawn like an answered question (the faint warn tint), without buttons. Recording it changes nothing else. byre asks for it (`readyPrompt`, sent as a byre message) when a task's state goes to `idle` or its workflow finishes (`Manager.Observe` calls `promptReady`) and the latest non-answer snapshot's review is `done` or `skipped`. It checks and sends under the session's delivery lock (the one human messages and answers take), and only to an agent that isn't running, so the prompt never queues behind a turn that could add unreviewed work. `tasks.ready_prompt_seq` holds the task's last event seq when the prompt's turn started; byre asks again only once a non-answer snapshot or a message from the human (a chat message or a question's answer, not one from byre, the agent or the lead) has come after it, so the agent's own turns that change no files don't ask again, and a human unblocking an agent that said it was waiting does. It doesn't ask while the task has an open question. It doesn't ask for a workflow stage task until its run has a `task.workflow_finished` event (by then the run's other stage tasks are being discarded, leaving the final one for the human), a task with a merge after review set, or a review that failed, and it skips the `reviewing` → `idle` change of a review with findings, since those go to the agent; the turn that answers them asks once it ends without new work. The prompt tells the agent not to call the tool, and to say what's left, when the task isn't ready.
|
||||
|
||||
An implementer that thinks its task shouldn't land at all (the approach failed, the task turned out unnecessary or already done) says so with the `suggest_discard` MCP tool (also `internal/agents/ready.go`), whose `reason` explains why; `readyPrompt` offers it as the alternative to `ready_for_merge`. Each call records a `task.discard_suggested` event, which the lead gets as a signal and the transcript draws with the same `ReadyBubble`, headed "suggests discarding". It takes part in "latest" with the ready-for-merge overviews, so whichever of the two came last is the current one, drawn and cleared under the same rules, and it carries only the Discard button. It only suggests: the task stays open until the human discards it.
|
||||
|
||||
@@ -342,11 +342,11 @@ An agent error (a turn that ends in error without a human interrupt, a process t
|
||||
|
||||
## Notifications
|
||||
|
||||
The running macOS app posts a notification for each new question, error and rate limit, raised by the app itself from the `inbox.created`, `session.error` and `session.rate_limited` events on its SSE stream (`notifyInbox` and `notifyError` in `mac/Sources/Byre/Notifications.swift`, through `UNUserNotificationCenter`). With the app not running, nothing notifies; closing its window leaves it running.
|
||||
The running macOS app posts notifications for events on its SSE stream, raised by the app itself (`notifyInbox`, `notifyError`, `notifyTurn` and `notifyTask` in `mac/Sources/Byre/Notifications.swift`, through `UNUserNotificationCenter`). With the app not running, nothing notifies; closing its window leaves it running.
|
||||
|
||||
- **Preferences.** `users.notification_prefs` holds what notifies the user, returned as `notification_prefs` on `GET /api/me` and set with `PUT /api/me/notifications`: `questions` (on by default), `reports` (task turns that end with `summary: true` on `turn.ended`, labelled "Turn summaries"), `errors` (`session.error`) and `rate_limits` (`session.rate_limited`). The app checks them against each event as it arrives.
|
||||
- **Content.** The title is `byre · <task title>` and the body the question (or the turn summary's first line, the error, or "Rate limited"), cut at 200 characters. The notification's identifier is the question's ID, `turn-<seq>` for a turn summary or `error-<seq>` for an error or rate limit, so a repeat replaces the earlier one.
|
||||
- **Clearing.** A delivered notification leaves Notification Center once it is dealt with anywhere, on this Mac, another one or by the lead: an `inbox.updated` event whose item is no longer `open` removes the item's notification (`clearInbox`), and a `summary.read` event removes that task's `turn-<seq>` notifications up to its `seq` (`clearTurns`).
|
||||
- **Preferences.** `users.notification_prefs` holds what notifies the user, returned as `notification_prefs` on `GET /api/me` and set with `PUT /api/me/notifications`: `questions` (`inbox.created`, on by default), `reports` (task turns that end with `summary: true` on `turn.ended`, labelled "Turn summaries"), `ready` (`task.ready`), `discard` (`task.discard_suggested`), `merge_problems` (`task.merge_stopped` and `task.merge_after_review_dropped`), `workflows` (`task.workflow_finished`), `errors` (`session.error`) and `rate_limits` (`session.rate_limited`). A key missing from the stored JSON reads as off. The app checks them against each event as it arrives.
|
||||
- **Content.** The title is `byre · <task title>` and the body the first line of the question, turn summary, ready-to-merge overview, discard or merge-problem reason, workflow output, or error (or "Rate limited"), cut at 200 characters. The notification's identifier is the question's ID, `turn-<seq>` for a turn summary, `workflow-<seq>` for a finished workflow, `error-<seq>` for an error or rate limit, and `task-<task id>` for ready to merge, suggests discard and merge problems, so each task shows at most one of those three at a time.
|
||||
- **Clearing.** A delivered notification leaves Notification Center once it is dealt with anywhere, on this Mac, another one or by the lead: an `inbox.updated` event whose item is no longer `open` removes the item's notification (`clearInbox`), a `summary.read` event removes that task's `turn-<seq>` notifications up to its `seq` (`clearTurns`), and `task.merged`, `task.discarded`, `snapshot.created` (unless its `trigger` is `answer`) and `inbox.created` remove the task's `task-<task id>` notification (`clearTask`); the last two are the events that clear the task's ready state on the server.
|
||||
- **Clicking** a notification activates the app, shows its window and points the right pane at the item's task.
|
||||
|
||||
The left pane's **Settings** tab asks macOS for notification permission and edits the preferences, and also holds the Default model menu (see "Agent sessions") and the two Review models menus (see "Reviews").
|
||||
@@ -371,11 +371,12 @@ Answers reach the agent as one message, listing each answer under the question i
|
||||
|
||||
Task agents use the Macs the human has byre's app open on directly, with no approval, as they use the machine byre runs on. While it runs, the app (`mac/Sources/Byre/Remote.swift`, started with the global event stream and stopped with it) keeps a WebSocket open to `GET /api/clients/connect`, reconnecting 2s after it drops. The connection is keyed by its `Byre-Client` header and named by its `Byre-Host` header (both required); a newer connection with the same `Byre-Client` replaces the older one, while two apps open on one Mac each keep their own. The client tools take the host name, or the `id` from `list_clients`, which they need when more than one app is connected from that host (the call fails listing the ids). byre keeps the connections in memory (`internal/clients`), pinging each every 30s and dropping one that doesn't answer within 15s. The app pings too, every 10s, and reconnects when it has had no pong for 25s. That catches a socket byre dropped while the Mac slept: the close never reached the Mac, and without something sent on it the app would wait on it indefinitely. Over it byre sends `ClientRequest` text messages, each with a fresh `id`, and the app answers each with a `ClientReply` carrying that `id` (`error` when it couldn't carry it out), in any order, handling requests concurrently. A request whose client disconnects before replying fails with "<host> disconnected before replying".
|
||||
|
||||
Implementer sessions get eight MCP tools for it (`internal/agents/clients.go`). The app sets its WebSocket's `maximumMessageSize` to 256 MiB, the same as byre's read limit, so file contents fit in one message each way.
|
||||
Implementer sessions get nine MCP tools for it (`internal/agents/clients.go`). The app sets its WebSocket's `maximumMessageSize` to 256 MiB, the same as byre's read limit, so file contents fit in one message each way.
|
||||
|
||||
- `list_clients`: the status's `clients` (see "Status"), with each one's host, login, `id`, connection time, app commit and `last_action_at`; the most recently active is listed first and marked.
|
||||
- `client_bash`: `op: bash`. The app runs `/bin/zsh -l -c <command>` in the human's home directory through `Process`, which starts it in its own process group, stdin from `/dev/null` and stdout and stderr into one temporary file, and kills the group with `SIGKILL` after `timeout_ms` (default 120000, at most 600000), replying `exit_code` -1. Otherwise `exit_code` is the exit status, or 128 plus the signal. `output` is the file's last 64 KiB. The agent's call waits the timeout plus 30s for the reply. Background children the command leaves are not waited for or killed, and a command that quits the app keeps running.
|
||||
- `client_upload`: `op: upload`. `path` is checked against the worktree through an `os.Root`; byre registers a transfer naming the path's directory and base name under a random id for as long as the call waits (at most 10 minutes), and the app downloads `GET /api/clients/transfers/{transferId}`, which streams `tar -czf - -C <dir> -- <name>` (404 once the call has ended). The app unpacks it into a new `.byre-upload-<uuid>` directory beside `dest` (`~/` expanded, parent directories created), removes whatever is at `dest`, moves the archive's one entry there and removes the temporary directory.
|
||||
- `client_open`: a `client_upload` of `path` to `~/Library/Caches/byre/open/<random>/<path's base name>`, then an `op: bash` running `open` on it, so the file opens in that Mac's default app for its type. The folder is left in place for the human to keep or for macOS to clear.
|
||||
- `client_download`: `op: download`, the reverse. byre makes `dest`'s parent directories through an `os.Root` on the worktree and registers a receiving transfer (`Transfer.Receive`) for a fresh temporary directory; the app runs `tar -czf <temp file> -C <path's parent> -- <path's name>` and `PUT`s it to `/api/clients/transfers/{transferId}`, which writes it once (`O_EXCL`) and answers 204, and 404 for a sending transfer, one already received or one whose call has ended (`GET` likewise answers 404 for a receiving one). byre unpacks it into a new `.byre-download-<transfer id>` directory beside `dest`, removes whatever is at `dest`, renames the archive's one entry there and removes the temporary directories, all inside the `os.Root`.
|
||||
- `client_read`: `op: read_file`; the app replies with the file's bytes in `data`, reading on a detached task, off the main actor. It resolves symlinks, refuses anything but a regular file (a FIFO would block the open), and reads at most 64 MiB plus one byte, refusing a longer file, so the base64 reply stays well under byre's 256 MiB read limit, past which byre would drop the connection. A `.png`, `.jpg`, `.jpeg`, `.gif` or `.webp` path comes back as an image result; other files must be UTF-8 and come back as `cat -n` style numbered lines from `offset` (default 1) for `limit` lines (default 2000), with a closing line naming the next offset when lines remain.
|
||||
- `client_write`: `op: write_file` with `path` and `data`; on a detached task, off the main actor, the app resolves symlinks, refuses an existing path that isn't a regular file (a FIFO would block the write), creates the parent directories and writes the file in place (`Data.write` without `.atomic`), so an existing file keeps its permissions.
|
||||
@@ -406,7 +407,7 @@ The Changes view lists the files with their counts, opens a file's diff on click
|
||||
|
||||
The Files tab browses the selected task's worktree, read-only (`internal/server/files.go`). `GET /api/tasks/{taskId}/dir?path=` lists one directory, directories first and then by name, without entries named `.git`; `GET /api/tasks/{taskId}/file?path=` returns one regular file. Both take a slash-separated path relative to the worktree root and work only while the task is active (409 otherwise). Every path is opened through an `os.Root` on the worktree, so a path that leaves it, through `..`, an absolute path or a symlink at any depth, is a 400; a path with a `.git` component is also refused. A listing reports each entry as `dir`, `file` (with its size) or `other`, following symlinks that stay inside the worktree (`symlink` marks them); a symlink that leaves the worktree or points nowhere is `other` and can't be opened. Gitignored entries are listed with `ignored` set: one `git check-ignore --stdin` per listing (`git.Ignored`) over the entries' paths, taken from the directory's real path so a listing reached through an in-worktree symlink is checked where it really lives; untracked paths under an ignored directory count as ignored, tracked files never do. The view shows them dimmed with `ignored` beside them, and they open like anything else. A listing stops at `maxDirEntries` entries with `truncated` set. A file read sends at most `maxFileBytes`, cut back to a whole UTF-8 character, with `truncated` set when the file is longer; a file with a NUL byte or invalid UTF-8 in that part is `binary` and comes back with no text.
|
||||
|
||||
The view shows a breadcrumb from `worktree` down to the current directory or file, the directory's entries (a `../` row to go up), or the file's text with line numbers and a note when only its start is shown. A file whose name ends in `.md` is rendered as Markdown with the transcript's renderer (`MarkdownView`) instead, with relative image paths taken from the file's own directory. Where you are is kept per task in memory (`Store.filePlaces`), so switching tasks or tabs returns to it, but a relaunch starts at the root. It refetches what it shows 1.5s after the selected task's last tool result or turn end, keeping the scroll position.
|
||||
The view shows a breadcrumb from `worktree` down to the current directory or file, the directory's entries (a `../` row to go up), or the file's text with line numbers and a note when only its start is shown. A file whose name ends in `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp` or `.svg` is shown as an image, fetched whole from `GET /api/tasks/{taskId}/image` (see "Screenshots from implementers") again on each refetch, fitted to the pane width and opening full size on click. A file whose name ends in `.md` is rendered as Markdown with the transcript's renderer (`MarkdownView`) instead, with relative image paths taken from the file's own directory. Where you are is kept per task in memory (`Store.filePlaces`), so switching tasks or tabs returns to it, but a relaunch starts at the root. It refetches what it shows 1.5s after the selected task's last tool result or turn end, keeping the scroll position.
|
||||
|
||||
Markdown rendered with a task (its transcript, questions, reviews, Markdown files in Files) links the worktree paths it mentions to Files (`InlineRenderer` in `mac/Sources/Byre/MarkdownView.swift`): a relative Markdown link (`[x](docs/a.md)`, from the file's own directory inside Files), a code span holding a path with a `/` that ends in a file name with an extension or in `/` (`` `mac/Sources/` ``), and the same kind of path in plain text, file names only. An absolute path counts when it starts with the task's worktree. A `:line`, `:line:col` or `#L12` suffix is dropped. The link is `byre-app://file?task=&path=`, which sets the task's place in Files to that file (or directory, for a trailing `/`) and focuses the task's Files tab. Nothing checks that the path exists; one that doesn't opens Files on its 404.
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -77,6 +78,22 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
|
||||
return m.clientUpload(ctx, l, raw)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "client_open",
|
||||
Description: "Show the human a file from your worktree on a client Mac: copy it into a fresh folder under ~/Library/Caches/byre/open/ there and open it with macOS's `open`, in that Mac's default app for its type (Preview for images and PDFs, the browser for HTML). " +
|
||||
"Use it when the human asks to see a file on their Mac, or for a file the Files tab can't show; it brings an app forward on their screen.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client": {"type": "string", "description": "The client's name from list_clients, or its id when several are connected from that Mac."},
|
||||
"path": {"type": "string", "description": "The file or directory, relative to your worktree."}
|
||||
},
|
||||
"required": ["client", "path"]
|
||||
}`),
|
||||
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
return m.clientOpen(ctx, l, raw)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "client_download",
|
||||
Description: "Copy a file or directory from a client Mac into your worktree, replacing whatever is at dest there. " +
|
||||
@@ -264,10 +281,17 @@ func (m *Manager) clientUpload(ctx context.Context, l *live, raw json.RawMessage
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
clean, err := m.upload(ctx, l, args.Client, args.Path, args.Dest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Copied %s to %s on %s.", clean, args.Dest, args.Client), nil
|
||||
}
|
||||
|
||||
clean := filepath.Clean(args.Path)
|
||||
func (m *Manager) upload(ctx context.Context, l *live, client, path, dest string) (string, error) {
|
||||
clean := filepath.Clean(path)
|
||||
if filepath.IsAbs(clean) {
|
||||
return "", fmt.Errorf("path %q must be relative to your worktree", args.Path)
|
||||
return "", fmt.Errorf("path %q must be relative to your worktree", path)
|
||||
}
|
||||
root, err := os.OpenRoot(l.dir)
|
||||
if err != nil {
|
||||
@@ -276,7 +300,7 @@ func (m *Manager) clientUpload(ctx context.Context, l *live, raw json.RawMessage
|
||||
_, err = root.Lstat(clean)
|
||||
root.Close()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("path %q: %w", args.Path, err)
|
||||
return "", fmt.Errorf("path %q: %w", path, err)
|
||||
}
|
||||
|
||||
full := filepath.Join(l.dir, clean)
|
||||
@@ -285,11 +309,47 @@ func (m *Manager) clientUpload(ctx context.Context, l *live, raw json.RawMessage
|
||||
|
||||
cctx, cancel := context.WithTimeout(ctx, copyTimeout)
|
||||
defer cancel()
|
||||
_, err = m.clients.Call(cctx, args.Client, api.ClientRequest{Op: api.Upload, Transfer: &id, Dest: &args.Dest})
|
||||
_, err = m.clients.Call(cctx, client, api.ClientRequest{Op: api.Upload, Transfer: &id, Dest: &dest})
|
||||
return clean, err
|
||||
}
|
||||
|
||||
type clientOpenArgs struct {
|
||||
Client string `json:"client"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func (m *Manager) clientOpen(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
|
||||
args := clientOpenArgs{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("malformed client_open call: %w", err)
|
||||
}
|
||||
dir := "~/Library/Caches/byre/open/" + rand.Text()
|
||||
name := filepath.Base(filepath.Clean(args.Path))
|
||||
if name == "." {
|
||||
return "", errors.New("path names your worktree, not a file in it")
|
||||
}
|
||||
dest := dir + "/" + name
|
||||
clean, err := m.upload(ctx, l, args.Client, args.Path, dest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Copied %s to %s on %s.", clean, args.Dest, args.Client), nil
|
||||
|
||||
command := "open " + dir + "/'" + strings.ReplaceAll(name, "'", `'\''`) + "'"
|
||||
ms := int(fileTimeout.Milliseconds())
|
||||
cctx, cancel := context.WithTimeout(ctx, fileTimeout+replyGrace)
|
||||
defer cancel()
|
||||
reply, err := m.clients.Call(cctx, args.Client, api.ClientRequest{Op: api.Bash, Command: &command, TimeoutMs: &ms})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if reply.ExitCode == nil || reply.Output == nil {
|
||||
return "", fmt.Errorf("%s's reply has no exit_code or output", args.Client)
|
||||
}
|
||||
if *reply.ExitCode != 0 {
|
||||
return "", fmt.Errorf("copied %s to %s on %s, but open exited %d: %s", clean, dest, args.Client, *reply.ExitCode, *reply.Output)
|
||||
}
|
||||
return fmt.Sprintf("Opened %s on %s, copied to %s.", clean, args.Client, dest), nil
|
||||
}
|
||||
|
||||
func (m *Manager) clientDownload(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
|
||||
|
||||
@@ -326,6 +326,9 @@ func (m *Manager) Observe(events []api.Event) {
|
||||
if ev.Kind == "task.state" && ev.Payload["to"] == string(api.TaskStateIdle) && ev.TaskId != nil {
|
||||
go m.promptReady(*ev.TaskId, ev.Payload["from"])
|
||||
}
|
||||
if ev.Kind == "task.workflow_finished" && ev.TaskId != nil {
|
||||
go m.promptReady(*ev.TaskId, nil)
|
||||
}
|
||||
switch ev.Kind {
|
||||
case "inbox.created", "inbox.updated", "task.created", "task.merge_queued", "task.merge_after_review", "task.merge_after_review_cancelled", "task.merge_after_review_dropped", "task.merge_started", "task.merged", "task.discarded", "turn.ended", "task.workflow_stage", "task.workflow_finished", "review.started", "review.finished", "task.state", "task.ready", "task.discard_suggested":
|
||||
case "user.message":
|
||||
|
||||
@@ -117,9 +117,15 @@ func (m *Manager) tryPromptReady(ctx context.Context, taskID string, fromReview
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if task.Status != api.TaskStatusActive || task.State != api.TaskStateIdle || task.WorkflowStage != nil || task.MergeAfterReview {
|
||||
if task.Status != api.TaskStatusActive || task.State != api.TaskStateIdle || task.MergeAfterReview {
|
||||
return nil
|
||||
}
|
||||
if task.WorkflowStage != nil {
|
||||
finished, err := m.store.WorkflowFinished(ctx, *task.WorkflowRun)
|
||||
if err != nil || !finished {
|
||||
return err
|
||||
}
|
||||
}
|
||||
sess, err = m.store.GetSession(ctx, sess.Id)
|
||||
if err != nil || sess.State == api.AgentSessionStateRunning || m.interrupted(sess.Id) {
|
||||
return err
|
||||
@@ -150,15 +156,23 @@ func (m *Manager) tryPromptReady(ctx context.Context, taskID string, fromReview
|
||||
return nil
|
||||
}
|
||||
}
|
||||
prompted, err := m.store.ReadyPromptN(ctx, taskID)
|
||||
if err != nil || prompted >= latest.N {
|
||||
due, err := m.store.ReadyPromptDue(ctx, taskID)
|
||||
if err != nil || !due {
|
||||
return err
|
||||
}
|
||||
open, err := m.store.ListInbox(ctx, store.InboxFilter{TaskID: taskID, State: api.Open})
|
||||
if err != nil || len(open) > 0 {
|
||||
return err
|
||||
}
|
||||
_, err = m.Send(ctx, sess.Id, "byre", readyPrompt, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = m.store.SetReadyPromptN(ctx, taskID, latest.N)
|
||||
seq, err := m.store.LastTaskSeq(ctx, taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = m.store.SetReadyPromptSeq(ctx, taskID, seq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+221
-208
@@ -998,19 +998,31 @@ type MessageCreate struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// NotificationPrefs Which new questions, finished turns, errors and rate limits notify this user in their open byre pages.
|
||||
// NotificationPrefs Which task events notify this user in the macOS app.
|
||||
type NotificationPrefs struct {
|
||||
// Discard task.discard_suggested events.
|
||||
Discard bool `json:"discard"`
|
||||
|
||||
// Errors session.error events on tasks.
|
||||
Errors bool `json:"errors"`
|
||||
|
||||
// MergeProblems task.merge_stopped and task.merge_after_review_dropped events.
|
||||
MergeProblems bool `json:"merge_problems"`
|
||||
|
||||
// Questions Questions. On by default.
|
||||
Questions bool `json:"questions"`
|
||||
|
||||
// RateLimits session.rate_limited events on tasks.
|
||||
RateLimits bool `json:"rate_limits"`
|
||||
|
||||
// Ready task.ready events.
|
||||
Ready bool `json:"ready"`
|
||||
|
||||
// Reports Task turns that end with a summary (turn.ended with summary: true).
|
||||
Reports bool `json:"reports"`
|
||||
|
||||
// Workflows task.workflow_finished events.
|
||||
Workflows bool `json:"workflows"`
|
||||
}
|
||||
|
||||
// PaneFocus defines model for PaneFocus.
|
||||
@@ -1502,7 +1514,7 @@ type User struct {
|
||||
GitName string `json:"git_name"`
|
||||
Login string `json:"login"`
|
||||
|
||||
// NotificationPrefs Which new questions, finished turns, errors and rate limits notify this user in their open byre pages.
|
||||
// NotificationPrefs Which task events notify this user in the macOS app.
|
||||
NotificationPrefs NotificationPrefs `json:"notification_prefs"`
|
||||
|
||||
// ReviewModel The model that reviews this user's tasks
|
||||
@@ -4095,212 +4107,213 @@ var swaggerSpec = []string{
|
||||
"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",
|
||||
"cJ/fq1rP+/f/cwA=",
|
||||
"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",
|
||||
"+XQ5rXdoxt5SzYP7/F7Vet6//58DAA==",
|
||||
}
|
||||
|
||||
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE tasks ADD COLUMN ready_prompt_seq INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE tasks SET ready_prompt_seq = COALESCE((
|
||||
SELECT MAX(e.seq) FROM events e
|
||||
WHERE e.task_id = tasks.id AND e.kind = 'user.message' AND e.actor = 'byre'
|
||||
AND json_extract(e.payload, '$.text') LIKE 'Your turn has ended and the review of your latest work has finished.%'
|
||||
), 0);
|
||||
|
||||
ALTER TABLE tasks DROP COLUMN ready_prompt_n;
|
||||
+11
-9
@@ -746,17 +746,19 @@ func (s *Store) SetTaskSummary(ctx context.Context, id, summary, actor string) e
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) ReadyPromptN(ctx context.Context, id string) (int, error) {
|
||||
n := 0
|
||||
err := s.db.QueryRowContext(ctx, `SELECT ready_prompt_n FROM tasks WHERE id = ?`, id).Scan(&n)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, ErrNotFound
|
||||
}
|
||||
return n, err
|
||||
func (s *Store) ReadyPromptDue(ctx context.Context, id string) (bool, error) {
|
||||
due := false
|
||||
err := s.db.QueryRowContext(ctx, `SELECT EXISTS (
|
||||
SELECT 1 FROM events e, tasks t
|
||||
WHERE t.id = ? AND e.task_id = t.id AND e.seq > t.ready_prompt_seq
|
||||
AND ((e.kind = 'snapshot.created' AND json_extract(e.payload, '$.trigger') != ?)
|
||||
OR (e.kind = 'user.message' AND e.actor NOT IN ('byre', ?) AND e.actor NOT LIKE '% (lead)'))
|
||||
)`, id, string(api.SnapshotTriggerAnswer), agentActor).Scan(&due)
|
||||
return due, err
|
||||
}
|
||||
|
||||
func (s *Store) SetReadyPromptN(ctx context.Context, id string, snapshotN int) error {
|
||||
_, err := s.db.ExecContext(ctx, `UPDATE tasks SET ready_prompt_n = ? WHERE id = ?`, snapshotN, id)
|
||||
func (s *Store) SetReadyPromptSeq(ctx context.Context, id string, seq int64) error {
|
||||
_, err := s.db.ExecContext(ctx, `UPDATE tasks SET ready_prompt_seq = ? WHERE id = ?`, seq, id)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,12 @@ private func bytes(_ n: Int64) -> String {
|
||||
return "\((Double(n) / 1024 / 1024).formatted(.number.precision(.fractionLength(1)).grouping(.never))) MiB"
|
||||
}
|
||||
|
||||
private let imageExts: Set<String> = ["png", "jpg", "jpeg", "gif", "webp", "svg"]
|
||||
|
||||
private func isImage(_ path: String) -> Bool {
|
||||
imageExts.contains((path as NSString).pathExtension.lowercased())
|
||||
}
|
||||
|
||||
private func shown(_ c: FileContent) -> Int64 {
|
||||
Int64(c.text.utf8.count)
|
||||
}
|
||||
@@ -56,6 +62,7 @@ private final class FilesModel {
|
||||
private(set) var content: FileContent?
|
||||
private(set) var error = ""
|
||||
private(set) var generation = 0
|
||||
private(set) var loads = 0
|
||||
@ObservationIgnored private var target = 0
|
||||
@ObservationIgnored private var timer: Task<Void, Never>?
|
||||
|
||||
@@ -104,6 +111,7 @@ private final class FilesModel {
|
||||
if started == target {
|
||||
content = fresh
|
||||
listing = nil
|
||||
loads += 1
|
||||
}
|
||||
} else {
|
||||
let fresh = try await store.api.client.getTaskDir(path: .init(taskId: taskId), query: .init(path: p.dir)).ok.body.json
|
||||
@@ -142,6 +150,9 @@ private final class FilesModel {
|
||||
return ""
|
||||
}
|
||||
if let c = content {
|
||||
if place.file.map(isImage) == true {
|
||||
return bytes(c.size)
|
||||
}
|
||||
if c.binary {
|
||||
return "\(bytes(c.size)) · binary"
|
||||
}
|
||||
@@ -229,11 +240,14 @@ private struct FilesBody: View {
|
||||
EmptyText(text: "No task selected.")
|
||||
} else if !model.error.isEmpty {
|
||||
EmptyText(text: model.error, error: true)
|
||||
} else if let c = model.content {
|
||||
if c.binary {
|
||||
} else if let c = model.content, let taskId = store.selectedTaskId {
|
||||
if let file = model.place.file, isImage(file) {
|
||||
FileImage(taskId: taskId, path: file, load: model.loads)
|
||||
.id(model.generation)
|
||||
} else if c.binary {
|
||||
EmptyText(text: "Binary file.")
|
||||
} else if model.place.file?.lowercased().hasSuffix(".md") == true {
|
||||
FileMarkdown(content: c, taskId: store.selectedTaskId, dir: model.place.dir)
|
||||
FileMarkdown(content: c, taskId: taskId, dir: model.place.dir)
|
||||
.id(model.generation)
|
||||
} else {
|
||||
FileText(content: c)
|
||||
@@ -340,6 +354,58 @@ private struct FileMarkdown: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct FileImage: View {
|
||||
let taskId: String
|
||||
let path: String
|
||||
let load: Int
|
||||
@Environment(Store.self) private var store
|
||||
@Environment(Overlays.self) private var overlays
|
||||
@ViewState private var image: NSImage?
|
||||
@ViewState private var error = ""
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let image {
|
||||
ScrollView {
|
||||
Image(nsImage: image)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: image.size.width, maxHeight: image.size.height)
|
||||
.overlay(Rectangle().stroke(Palette.border))
|
||||
.onTapGesture {
|
||||
overlays.image = image
|
||||
}
|
||||
.help("Open full size")
|
||||
.padding(16)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
} else if !error.isEmpty {
|
||||
EmptyText(text: error, error: true)
|
||||
} else {
|
||||
EmptyText(text: "Loading…")
|
||||
}
|
||||
}
|
||||
.task(id: load) {
|
||||
do {
|
||||
let data = try await ImageCache.fetch(.worktree(taskId: taskId, path: path), store.api)
|
||||
guard let img = NSImage(data: data) else {
|
||||
image = nil
|
||||
error = "Not a readable image."
|
||||
return
|
||||
}
|
||||
image = img
|
||||
error = ""
|
||||
} catch {
|
||||
if Task.isCancelled {
|
||||
return
|
||||
}
|
||||
image = nil
|
||||
self.error = errorText(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct FileText: View {
|
||||
let content: FileContent
|
||||
|
||||
|
||||
@@ -118,6 +118,37 @@ func clearTurns(_ taskId: String, through seq: Int64) {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func notifyTask(_ store: Store, _ ev: ByreEvent) {
|
||||
guard let me = store.me, let taskId = ev.taskId, !taskId.isEmpty else {
|
||||
return
|
||||
}
|
||||
let prefs = me.notificationPrefs
|
||||
let origin = store.api.origin.absoluteString
|
||||
switch ev.kind {
|
||||
case "task.ready" where prefs.ready:
|
||||
show(store, taskId, "task-\(taskId)@\(origin)", "Ready to merge: \(str(ev.payload.value, "overview") ?? "")")
|
||||
case "task.discard_suggested" where prefs.discard:
|
||||
show(store, taskId, "task-\(taskId)@\(origin)", "Suggests discard: \(str(ev.payload.value, "reason") ?? "")")
|
||||
case "task.merge_stopped" where prefs.mergeProblems:
|
||||
show(store, taskId, "task-\(taskId)@\(origin)", "Merge stopped: \(str(ev.payload.value, "reason") ?? "")")
|
||||
case "task.merge_after_review_dropped" where prefs.mergeProblems:
|
||||
show(store, taskId, "task-\(taskId)@\(origin)", "Didn't merge after the review: \(str(ev.payload.value, "reason") ?? "")")
|
||||
case "task.workflow_finished" where prefs.workflows:
|
||||
show(store, taskId, "workflow-\(ev.seq)@\(origin)", "Finished \(str(ev.payload.value, "workflow") ?? ""): \(str(ev.payload.value, "output") ?? "")")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func clearTask(_ store: Store, _ taskId: String) {
|
||||
let id = "task-\(taskId)@\(store.api.origin.absoluteString)"
|
||||
enqueue {
|
||||
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [id])
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func notifyTurn(_ store: Store, _ ev: ByreEvent) {
|
||||
guard let me = store.me, me.notificationPrefs.reports, ev.payload.value["summary"] as? Bool == true, let taskId = ev.taskId, !taskId.isEmpty else {
|
||||
|
||||
@@ -7,9 +7,13 @@ typealias NotificationPrefs = Components.Schemas.NotificationPrefs
|
||||
@MainActor
|
||||
private let kinds: [(key: WritableKeyPath<NotificationPrefs, Bool>, label: String)] = [
|
||||
(\.questions, "Questions"),
|
||||
(\.reports, "Turn summaries"),
|
||||
(\.ready, "Ready to merge"),
|
||||
(\.discard, "Suggests discard"),
|
||||
(\.mergeProblems, "Merge problems"),
|
||||
(\.workflows, "Workflow finished"),
|
||||
(\.errors, "Session errors"),
|
||||
(\.rateLimits, "Rate limits"),
|
||||
(\.reports, "Turn summaries"),
|
||||
]
|
||||
|
||||
struct SettingsView: View {
|
||||
|
||||
@@ -624,6 +624,10 @@ final class Store {
|
||||
try await self.loadProjects()
|
||||
}
|
||||
case "task.created", "task.merge_title", "task.merge_queued", "task.merge_unqueued", "task.merge_after_review", "task.merge_after_review_ended", "task.merge_after_review_cancelled", "task.parent_merge_started", "task.parent_merge_stopped", "task.merge_started", "task.merge_ended", "task.discard_started", "task.merged", "task.discarded", "task.state", "task.ready", "task.discard_suggested", "snapshot.created":
|
||||
if let taskId = ev.taskId, ["task.merged", "task.discarded"].contains(ev.kind) || (ev.kind == "snapshot.created" && str(ev.payload.value, "trigger") != "answer") {
|
||||
clearTask(self, taskId)
|
||||
}
|
||||
notifyTask(self, ev)
|
||||
if let projectId = ev.projectId {
|
||||
spawn("load tasks") {
|
||||
try await self.loadTasks(projectId)
|
||||
@@ -640,6 +644,9 @@ final class Store {
|
||||
}
|
||||
}
|
||||
case "inbox.created":
|
||||
if let taskId = ev.taskId {
|
||||
clearTask(self, taskId)
|
||||
}
|
||||
notifyInbox(self, try! decodePayload(ev.payload, as: InboxItem.self))
|
||||
spawn("load inbox") {
|
||||
try await self.loadInbox()
|
||||
@@ -656,6 +663,8 @@ final class Store {
|
||||
}
|
||||
case "session.error", "session.rate_limited":
|
||||
notifyError(self, ev)
|
||||
case "task.merge_stopped", "task.merge_after_review_dropped", "task.workflow_finished":
|
||||
notifyTask(self, ev)
|
||||
case "turn.ended":
|
||||
notifyTurn(self, ev)
|
||||
if ev.payload.value["summary"] as? Bool == true, let taskId = ev.taskId {
|
||||
|
||||
@@ -1,12 +1,59 @@
|
||||
import AppKit
|
||||
import Sparkle
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class UpdateProgress {
|
||||
var title = ""
|
||||
var done = 0.0
|
||||
var total: Double?
|
||||
var detail = ""
|
||||
var cancel: (() -> Void)?
|
||||
}
|
||||
|
||||
private struct UpdateProgressView: View {
|
||||
let progress: UpdateProgress
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text(progress.title)
|
||||
.font(Fonts.font(13, weight: 700))
|
||||
.foregroundStyle(Palette.text)
|
||||
if let total = progress.total {
|
||||
ProgressView(value: min(progress.done, total), total: total)
|
||||
.tint(Palette.accent)
|
||||
} else {
|
||||
ProgressView()
|
||||
.progressViewStyle(.linear)
|
||||
.tint(Palette.accent)
|
||||
}
|
||||
HStack {
|
||||
Text(progress.detail)
|
||||
.font(Fonts.font(12))
|
||||
.foregroundStyle(Palette.muted)
|
||||
Spacer()
|
||||
if let cancel = progress.cancel {
|
||||
Button("Cancel", action: cancel)
|
||||
.buttonStyle(WebButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.frame(width: 360)
|
||||
.background(Palette.bg.ignoresSafeArea())
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class Updater: NSObject, SPUUserDriver, SPUUpdaterDelegate {
|
||||
static let shared = Updater()
|
||||
private var updater: SPUUpdater?
|
||||
private var now = false
|
||||
private var version = ""
|
||||
private var staged: (item: SUAppcastItem, install: () -> Void)?
|
||||
private let progress = UpdateProgress()
|
||||
private var panel: NSPanel?
|
||||
|
||||
func start() {
|
||||
let updater = SPUUpdater(hostBundle: .main, applicationBundle: .main, userDriver: self, delegate: self)
|
||||
@@ -20,7 +67,8 @@ final class Updater: NSObject, SPUUserDriver, SPUUpdaterDelegate {
|
||||
|
||||
func check() {
|
||||
if let staged {
|
||||
if ask(staged.item) {
|
||||
now = ask(staged.item)
|
||||
if now {
|
||||
staged.install()
|
||||
}
|
||||
return
|
||||
@@ -29,6 +77,7 @@ final class Updater: NSObject, SPUUserDriver, SPUUpdaterDelegate {
|
||||
}
|
||||
|
||||
private func ask(_ item: SUAppcastItem) -> Bool {
|
||||
version = item.displayVersionString
|
||||
let current = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Byre \(item.displayVersionString) is available"
|
||||
@@ -47,9 +96,38 @@ final class Updater: NSObject, SPUUserDriver, SPUUpdaterDelegate {
|
||||
alert.runModal()
|
||||
}
|
||||
|
||||
private func showProgress(_ title: String, total: Double?, cancel: (() -> Void)? = nil) {
|
||||
progress.title = title
|
||||
progress.done = 0
|
||||
progress.total = total
|
||||
progress.detail = ""
|
||||
progress.cancel = cancel
|
||||
if panel == nil {
|
||||
let p = NSPanel(contentRect: .zero, styleMask: [.titled, .fullSizeContentView], backing: .buffered, defer: false)
|
||||
p.titleVisibility = .hidden
|
||||
p.titlebarAppearsTransparent = true
|
||||
p.isReleasedWhenClosed = false
|
||||
p.contentViewController = NSHostingController(rootView: UpdateProgressView(progress: progress))
|
||||
p.center()
|
||||
panel = p
|
||||
}
|
||||
NSApp.activate()
|
||||
panel?.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
private func hideProgress() {
|
||||
panel?.orderOut(nil)
|
||||
panel = nil
|
||||
}
|
||||
|
||||
private func bytes(_ n: Double) -> String {
|
||||
Int64(n).formatted(.byteCount(style: .file))
|
||||
}
|
||||
|
||||
func updater(_ updater: SPUUpdater, willInstallUpdateOnQuit item: SUAppcastItem, immediateInstallationBlock immediateInstallHandler: @escaping () -> Void) -> Bool {
|
||||
staged = (item, immediateInstallHandler)
|
||||
if ask(item) {
|
||||
now = ask(item)
|
||||
if now {
|
||||
immediateInstallHandler()
|
||||
}
|
||||
return true
|
||||
@@ -80,29 +158,66 @@ final class Updater: NSObject, SPUUserDriver, SPUUpdaterDelegate {
|
||||
}
|
||||
|
||||
func showUpdaterError(_ error: any Error, acknowledgement: @escaping () -> Void) {
|
||||
hideProgress()
|
||||
tell("Update failed", error.localizedDescription)
|
||||
acknowledgement()
|
||||
}
|
||||
|
||||
func showDownloadInitiated(cancellation: @escaping () -> Void) {}
|
||||
func showDownloadInitiated(cancellation: @escaping () -> Void) {
|
||||
guard now else {
|
||||
return
|
||||
}
|
||||
showProgress("Downloading Byre \(version)", total: nil) { [weak self] in
|
||||
self?.now = false
|
||||
self?.hideProgress()
|
||||
cancellation()
|
||||
}
|
||||
}
|
||||
|
||||
func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) {}
|
||||
func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) {
|
||||
guard expectedContentLength > 0 else {
|
||||
return
|
||||
}
|
||||
progress.total = Double(expectedContentLength)
|
||||
}
|
||||
|
||||
func showDownloadDidReceiveData(ofLength length: UInt64) {}
|
||||
func showDownloadDidReceiveData(ofLength length: UInt64) {
|
||||
progress.done += Double(length)
|
||||
if let total = progress.total {
|
||||
progress.detail = "\(bytes(progress.done)) of \(bytes(total))"
|
||||
} else {
|
||||
progress.detail = bytes(progress.done)
|
||||
}
|
||||
}
|
||||
|
||||
func showDownloadDidStartExtractingUpdate() {}
|
||||
func showDownloadDidStartExtractingUpdate() {
|
||||
guard now else {
|
||||
return
|
||||
}
|
||||
showProgress("Extracting Byre \(version)", total: 1)
|
||||
}
|
||||
|
||||
func showExtractionReceivedProgress(_ progress: Double) {}
|
||||
func showExtractionReceivedProgress(_ progress: Double) {
|
||||
self.progress.done = progress
|
||||
}
|
||||
|
||||
func showReady(toInstallAndRelaunch reply: @escaping (SPUUserUpdateChoice) -> Void) {
|
||||
reply(now ? .install : .dismiss)
|
||||
}
|
||||
|
||||
func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) {}
|
||||
func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) {
|
||||
guard now else {
|
||||
return
|
||||
}
|
||||
showProgress("Installing Byre \(version)", total: nil)
|
||||
}
|
||||
|
||||
func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) {
|
||||
hideProgress()
|
||||
acknowledgement()
|
||||
}
|
||||
|
||||
func dismissUpdateInstallation() {}
|
||||
func dismissUpdateInstallation() {
|
||||
hideProgress()
|
||||
}
|
||||
}
|
||||
|
||||
+1181
-39
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user