19 Commits
Author SHA1 Message Date
Ian Gulliver 40fffdf839 Push the initial commit byre makes in an empty repo 2026-10-22 12:59:40 -07:00
Ian Gulliver b8984d30f0 Start tasks in a repo with no commits 2026-10-22 11:46:48 -07:00
Ian Gulliver 48741e8cbb Load only active tasks on connect and refetch single tasks on events 2026-10-22 11:43:35 -07:00
Ian Gulliver 78fcdfff34 Make ⌘P open the task list instead of the project page 2026-10-22 11:01:00 -07:00
Ian Gulliver 643b04b760 Let the user name the iPhone in Settings 2026-10-22 10:43:24 -07:00
Ian Gulliver 8fb27f98a2 Remove unused Codex protocol registries and session APIs 2026-10-22 10:38:02 -07:00
Ian Gulliver 406f67a1a0 Show the phone's task list in the Mac's right pane from a list button 2026-10-22 10:31:49 -07:00
Ian Gulliver b54a1a04b3 Keep diagram buttons the same size when selected 2026-10-22 10:13:40 -07:00
Ian Gulliver 33604e2a51 Keep the iPhone's name out of its tailnet hostname 2026-10-22 10:10:34 -07:00
Ian Gulliver 08fe45814e Read the iPhone's own name for its host and tailnet node 2026-10-22 09:25:15 -07:00
Ian Gulliver fedd4ad673 Start a new task from a closed task 2026-10-22 09:05:24 -07:00
Ian Gulliver 88c0199845 Unify pane hold state and expiry handling 2026-10-22 08:49:34 -07:00
Ian Gulliver 22f73a3bad Show the arrow cursor over buttons 2026-10-22 08:42:54 -07:00
Ian Gulliver 17f2706ba4 Relaunch running client apps when deploying 2026-10-22 08:39:36 -07:00
Ian Gulliver 6b09ffb362 Update client Macs before the server and keep a copy of each deploy 2026-10-22 08:17:33 -07:00
Ian Gulliver 519199eba4 Run the tailnet SOCKS proxy in Swift so the iPhone app reconnects 2026-10-22 08:11:32 -07:00
Ian Gulliver e28071f380 Unify schedule creation and editing lifecycle 2026-10-22 08:07:22 -07:00
Ian Gulliver 445f56881c Fix submitting the merge card's Other text on iPhone 2026-10-22 08:04:04 -07:00
Ian Gulliver a3d1852f45 Trigger folder access prompts in permission checks; add lead welcome 2026-10-22 08:01:59 -07:00
45 changed files with 1386 additions and 849 deletions
+11 -12
View File
@@ -12,27 +12,26 @@ These are requirements, not preferences. Changing one needs the owner's agreemen
A deploy is a release that isn't uploaded: the same notarized disk image, installed on the owner's Macs instead of published. When asked to deploy, do exactly this, from the worktree root:
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>/`), builds the iPhone app with `mac/phone.sh` and uploads it to TestFlight (`docs/iphone.md`), 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/tailnet` at the loopback address (`byre check` prints it as `loopback`), whose `state` must be `Running`. Report the new build number first (`plutil -extract CFBundleVersion raw /Applications/Byre.app/Contents/Info.plist` prints it; the iPhone build uploaded to TestFlight has the same one), then 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 build number (`install.sh` prints it, then the commit).
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.
1. Run `mac/deploy.sh`. It runs `mac/package.sh` (the build, both notarizations and the disk image, as for a release, into `mac/.build/release/<version>/`), builds the iPhone app with `mac/phone.sh` and uploads it to TestFlight (`docs/iphone.md`), and copies the disk image and `mac/install.sh` to `~/byre-deploy/byre.dmg` and `~/byre-deploy/install.sh`. That copy stays there for installing by hand on Macs that aren't connected. It installs nothing.
2. Sort the client Macs `list_clients` shows (leaving out the iPhone) by whether they run a byre server of their own: one does when `launchctl print gui/$(id -u)/run.fc.byre` exits 0 there (`client_bash`). Update the app on each one that runs no server, one Mac at a time, through the client tools and without asking first: upload `~/byre-deploy/byre.dmg` and `~/byre-deploy/install.sh` to the same paths there (`client_upload`), then run `~/byre-deploy/install.sh ~/byre-deploy/byre.dmg` there through `client_bash`. It installs, prints the build number and the commit, then quits the app if it is running and reopens it on the new build, which ends the `client_bash` call as the client disconnects while the script carries on. Wait until `list_clients` shows that Mac back on the new build and commit, and report them. Every one of these must be done before any server restarts, since a protocol change can leave an old client unable to reach a new server to be updated.
3. Only then update each client Mac that runs a server, the same way. Its server sees its executable replaced and restarts on the new build.
4. Install on the Mac you run on with `mac/install.sh ~/byre-deploy/byre.dmg`, always as the last action of the turn. 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. `install.sh` relaunches its app if it is running, and leaves it closed if not.
5. When you resume, confirm it came up: read the tail of `~/Library/Logs/byre.log`, and `GET /api/tailnet` at the loopback address (`byre check` prints it as `loopback`), whose `state` must be `Running`. Report the new build number first (`plutil -extract CFBundleVersion raw /Applications/Byre.app/Contents/Info.plist` prints it; the iPhone build uploaded to TestFlight has the same one), then what you deployed and what you checked.
6. 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.
Deploying means running `mac/deploy.sh` on iso. Never stand in for it by running `mac/package.sh` and `mac/install.sh` yourself, not even to build once for several Macs. Every other Mac gets the disk image `mac/deploy.sh` built, after iso is confirmed healthy (step 3).
Deploying means building with `mac/deploy.sh` on the Mac you run on. Never stand in for it by running `mac/package.sh` yourself, not even to build once for several Macs. Every Mac, this one included, gets the disk image `mac/deploy.sh` left in `~/byre-deploy/`.
Besides iso, beast also runs its own byre server:
- Upload the disk image to beast as `~/byre-deploy/byre.dmg` and `mac/install.sh` as `~/byre-deploy/install.sh` (`client_upload`), then run `~/byre-deploy/run.sh` there detached, through `client_bash`: `perl -MPOSIX -e 'fork and exit; POSIX::setsid(); open STDIN,"</dev/null"; open STDOUT,">>$ENV{HOME}/byre-deploy/run.log"; open STDERR,">&STDOUT"; exec "/bin/zsh","$ENV{HOME}/byre-deploy/run.sh"'`. It quits the app, installs, and reopens it; the beast client drops for about a minute and `list_clients` shows it back on the new commit. Do the same for every other running client Mac, and relaunch iso's app only if it is running.
- Check each server with the bearer token in `~/Library/Application Support/byre/loopback-token`: `GET https://byre-<host>.tailcecab.ts.net/api/tailnet` (`state` is `Running`) and `/api/admin/status` (report zombies, protocol errors, rate limits). The loopback address serves `/api/tailnet` but not `/api/admin/status`.
Check each server (this Mac's and each client Mac's from step 3) with the bearer token in `~/Library/Application Support/byre/loopback-token`: `GET https://byre-<host>.tailcecab.ts.net/api/tailnet` (`state` is `Running`) and `/api/admin/status` (report zombies, protocol errors, rate limits). The loopback address serves `/api/tailnet` but not `/api/admin/status`.
What to expect:
- The notarizations take a minute or two each; `notarytool` prints "In Progress" until "Accepted". A rejection stops the script before anything is installed; `xcrun notarytool log <id> --keychain-profile byre-notary` says why.
- You run inside byre, so the restart kills your own turn just after the install. That is expected, not a reason to skip it. You resume with a "byre restarted while you were mid-turn" message; do the checks then, and confirm the service started after your install (the log shows `executable replaced, exiting` just before it).
- The restart can cut off the record of the last tool call, so you may resume with no memory of having deployed. Before concluding that another agent restarted byre, compare the service's start time (the last `listening` line in the log) with when you ran `mac/deploy.sh`: a restart a few minutes after it, once the notarizations are done, is most likely your install. If byre restarts without your install, another agent deployed its own branch, and the running build is theirs.
- The restart can cut off the record of the last tool call, so you may resume with no memory of having deployed. Before concluding that another agent restarted byre, compare the service's start time (the last `listening` line in the log) with when you ran `mac/install.sh`: a restart within seconds of it is most likely your install. If byre restarts without your install, another agent deployed its own branch, and the running build is theirs.
## Releasing
A release is a deploy that is uploaded, and goes to every Mac through the app's updater. Make one only when the owner asks. It needs a `v<version>` tag on a commit of main, pushed by itself (`git push origin v<version>`, never moving or recreating a tag), and a clean checkout at that tag on iso, where `mac/release.sh <notes>` checks the tag, runs `mac/package.sh`, uploads the image, its `.sha256` and `appcast.xml` to `dl.byre.run`, and creates the release on g.fc.run with copies of the image and its `.sha256`, and the Markdown file `<notes>` as its notes. `docs/operations.md`, "Releases", has the details.
A release is a deploy that is uploaded, and goes to every Mac through the app's updater. Make one only when the owner asks. It needs a `v<version>` tag on a commit of main, pushed by itself (`git push origin v<version>`, never moving or recreating a tag), and a clean checkout at that tag on the Mac byre runs on, where `mac/release.sh <notes>` checks the tag, runs `mac/package.sh`, uploads the image, its `.sha256` and `appcast.xml` to `dl.byre.run`, and creates the release on g.fc.run with copies of the image and its `.sha256`, and the Markdown file `<notes>` as its notes. `docs/operations.md`, "Releases", has the details.
Before running it, write the notes yourself from the diffs since the previous tag (`git describe --tags --abbrev=0 v<version>^` names it; read `git diff <previous>..v<version>`, not just the commit subjects). Write the file outside the checkout, so the tree stays clean. It is a changelog for people who use byre, in this shape:
@@ -52,7 +51,7 @@ When the owner tells the lead "deploy byre", or anything that means the same, th
- Exactly two panes, side by side, each exactly half the width, at every window size. Never three or more columns, never a sidebar, never a resizable or collapsible split.
- No navbar. The panes fill the window, below only the identity banner when it shows.
- The left pane has six tabs: Lead (the user's lead agent), Status, Notes (what the lead has noted about the user), Workflows (the saved workflows), Server (the byre service on this Mac) and Settings (the user's and this client's preferences), with the connection dot and the user's name at the end of its tab strip. Nothing else of the user's goes in it. The right pane always shows one task's conversation (its transcript, or a workflow stage task's run), or a project's dashboard when no task is selected; the lead sets the task, and the human can switch it (⌘K). The task's other views (its other tabs, and images and diagrams zoomed from its transcript) open only when the human opens them, in a sheet over the left pane: attached to the center line, in the task's color and named by it, leaving a strip of the left pane visible that closes it. Images and diagrams zoomed from the lead chat open the same way over the right pane. A new task view is a new tab that opens in that sheet, never a new column or region.
- The left pane has six tabs: Lead (the user's lead agent), Status, Notes (what the lead has noted about the user), Workflows (the saved workflows), Server (the byre service on this Mac) and Settings (the user's and this client's preferences), with the connection dot and the user's name at the end of its tab strip. Nothing else of the user's goes in it. The right pane always shows one task's conversation (its transcript, or a workflow stage task's run), or a project's dashboard when no task is selected, or the list of every project's active tasks while the task bar's list button has it open; the lead sets the task, and the human can switch it (⌘K). The task's other views (its other tabs, and images and diagrams zoomed from its transcript) open only when the human opens them, in a sheet over the left pane: attached to the center line, in the task's color and named by it, leaving a strip of the left pane visible that closes it. Images and diagrams zoomed from the lead chat open the same way over the right pane. A new task view is a new tab that opens in that sheet, never a new column or region.
- Overlays (the task switcher, confirmation dialogs) are allowed; they sit above the panes and don't change the layout.
## UI look
+1 -1
View File
@@ -12,7 +12,7 @@ A server and macOS app for running many coding agents (Claude Code, Codex and An
- **Merge.** One click: the agent merges the default branch in and resolves conflicts, then byre squashes, pushes and closes the task, with co-author trailers and a git note recording the task. Merge after review, and one merge per project at a time with a queue.
- **Subtasks and teams.** Agents can spawn subtasks on their own branches and merge them back, or several agents can share one worktree and talk through a board.
- **Workflows.** Named sequences of stages that run in order or in parallel, with each stage running as its own task.
- **TODO.md.** A project's `TODO.md` items show on the project's dashboard (⌘P); start or discuss one in a click.
- **TODO.md.** A project's `TODO.md` items show on the project's dashboard; start or discuss one in a click.
- **Changes, Files and Terminal.** Per-task diff view with line comments sent to the agent, a read-only file browser, and shells in the worktree, including ones that start by running a saved command.
- **Previews.** Agents expose dev servers, and the app opens each at a local `*.localhost` address forwarded over the tailnet.
- **Screen and app control.** Agents can see and drive the Mac byre runs on (screen capture, accessibility, Orion, Chrome DevTools) and the Macs you have the app open on (shell, files, the same screen tools).
+33 -4
View File
@@ -337,10 +337,13 @@ paths:
parameters:
- { name: projectId, in: path, required: true, schema: { type: string } }
get:
operationId: listTasks
operationId: listActiveTasks
description: >
The project's active tasks. A closed task comes from getTask; task events name the task they change, which
clients fetch again with getTask.
responses:
"200":
description: The project's tasks, newest first.
description: The project's active tasks, newest first.
content:
application/json:
schema:
@@ -972,6 +975,32 @@ paths:
"404": { $ref: "#/components/responses/Error" }
"409": { $ref: "#/components/responses/Error" }
/api/sessions/{sessionId}/continue:
parameters:
- { name: sessionId, in: path, required: true, schema: { type: string } }
post:
operationId: continueSession
description: >
Creates a new task from a merged or discarded task's agent session: its branch and worktree start from the
project's default branch, with an agent session that continues that session's conversation from its end (the
vendor's own session fork) and gets the message as its first turn, as sendMessage does. The new task has
forked_from and no forked_at. Refused while the session's task is active, for a session that isn't a task's
implementer, and for an Antigravity session.
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/MessageCreate" }
responses:
"201":
description: The new task.
content:
application/json:
schema: { $ref: "#/components/schemas/Task" }
"400": { $ref: "#/components/responses/Error" }
"404": { $ref: "#/components/responses/Error" }
"409": { $ref: "#/components/responses/Error" }
/api/tasks/{taskId}/sessions:
parameters:
- { name: taskId, in: path, required: true, schema: { type: string } }
@@ -1367,7 +1396,7 @@ paths:
/api/admin/permissions:
post:
operationId: checkPermissions
description: Tests the service's macOS privacy grants by running byre-hands' check, which asks macOS for any grant not yet decided. Waits while macOS shows an Automation prompt, up to 50 seconds.
description: Tests the service's macOS privacy grants by running byre-hands' check, which asks macOS for any grant not yet decided. Waits while macOS shows its prompts (Automation, and access to the Desktop, Documents, Downloads and iCloud Drive folders), up to 2 minutes.
responses:
"200":
description: One result per grant.
@@ -1539,7 +1568,7 @@ components:
merge_commit: { type: string, description: "merged: the squash commit on the default branch; empty when there was nothing to merge." }
todo_item: { type: string, description: "The title of the TODO.md item the task was started from; absent for other tasks." }
forked_from: { type: string, description: "The task this one was forked from; absent for other tasks." }
forked_at: { type: integer, description: "With forked_from: the n of the snapshot it was forked at." }
forked_at: { type: integer, description: "With forked_from: the n of the snapshot it was forked at; absent when it continues a closed task from its end." }
merge_queued_at: { type: string, format: date-time, description: "When Merge was requested while another task in the project was merging; absent once the merge starts or when none is held." }
workflow: { type: string, description: "For a workflow run's task: the name of the workflow it froze at creation (getTaskWorkflow has its copy). A run's task has no agent, worktree or branch (branch, worktree_path and base_ref are empty); its stage tasks are its subtasks, and it closes once they all have, as merged when any of them merged. Absent for other tasks." }
workflow_stage: { type: string, description: "For a stage task of a workflow run: the name of the stage it runs; parent_task_id is the run's task. Absent for other tasks." }
+1 -1
View File
@@ -17,5 +17,5 @@ The simulator's window (Xcode's Device Hub) lists the app's own accessibility el
## Platform facts
- CloudKit on iOS requires the `remote-notification` background mode in `Info.plist`, even though the app sets up no push of its own.
- `ProcessInfo.hostName` is the Mac's name in the simulator and meaningless on a phone, so the phone's tailnet node and `Byre-Host` are named `iPhone`.
- The phone's name is the one typed in Settings, kept in the phone's own `UserDefaults` (not synced through iCloud, since each phone has its own), and `iPhone` until one is typed. It goes in `Byre-Host` and the tailnet node's name, both fixed when the app starts. `UIDevice.current.name` returns just `iPhone` without Apple's `com.apple.developer.device-information.user-assigned-device-name` entitlement, which Apple refused because byre shows the name to other people and sends it to Tailscale and to the agents' model vendors.
- SwiftTerm's iOS `TerminalViewDelegate` requires `requestOpenLink`, which its macOS one provides a default for.
+2 -1
View File
@@ -26,7 +26,8 @@ Every build is signed for distribution outside the App Store: the team's Develop
## 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".
- 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 made then fails.
- Every request reaches the tailnet through byre's own SOCKS5 proxy on a loopback port (`Proxy.swift`), which dials with `tailscale_dial`, not libtailscale's `tailscale_loopback`. iOS reclaims a suspended app's listening sockets, and tsnet's proxy stops for good on the first failed accept, leaving the phone unable to reach any server. byre's proxy binds again on the same port each time the app returns to the foreground.
- 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.
+4 -2
View File
@@ -12,6 +12,8 @@ Add SQL migrations in `internal/store/migrations/` as `NNN-name.sql`. The number
byre runs nothing in a new worktree. Installing dependencies or copying untracked files such as `.env` is the project's own instruction in its AGENTS.md, carried out by the agent with its own tools.
Closing a task deletes its branch, worktree and snapshot refs, so a task continued from a closed one (Send in a merged or discarded task, which becomes Continue) starts from the default branch: a merged task's work is there, a discarded task's is gone. Only the agent's conversation carries over, through the vendor's own session files, which outlive the worktree.
## Running as a service
The service always runs from the app: `/Applications/Byre.app/Contents/Helpers/byre install`, which the app's Server tab and connection screen run. `byre install` takes `PATH` from the shell it is run in, since the service's `PATH` must find `claude`, `codex`, `agy`, `git`, `curl` and `npx`. When the service is already loaded, `install` hands `launchctl bootout` and `bootstrap` to a detached `sh` and exits: `bootout` kills every process in the service's process groups, which includes `byre install` itself when an agent byre runs is the one running it.
@@ -36,7 +38,7 @@ Each preview gets its own port on byre's node rather than a path under byre's or
## Releases
A release is one notarized disk image, `byre-<version>.dmg`, holding `Byre.app` with the server, `byre-hands` and `byre-update` in `Contents/Helpers`. Images are served from `https://dl.byre.run/`; each release's notes are on `https://g.fc.run/ai/byre/releases`. `mac/package.sh` builds one, `mac/deploy.sh` installs it locally (see `AGENTS.md`, "Deploying"), and `mac/release.sh <notes>` publishes it from a clean checkout at a pushed `v<version>` tag. The build number (`CFBundleVersion`) of both the Mac and iPhone apps is the Unix time of the commit they were built from (`git log -1 --format=%ct`), so one deploy's apps share it; the apps send it in their `Byre-Build` header and the Status tab's client list shows it. A deployed build's `CFBundleVersion` is at least the last release's, so the updater never offers to go back to it.
A release is one notarized disk image, `byre-<version>.dmg`, holding `Byre.app` with the server, `byre-hands` and `byre-update` in `Contents/Helpers`. Images are served from `https://dl.byre.run/`; each release's notes are on `https://g.fc.run/ai/byre/releases`. `mac/package.sh` builds one, `mac/deploy.sh` builds one for installing on the owner's Macs and keeps a copy in `~/byre-deploy/` (see `AGENTS.md`, "Deploying"), and `mac/release.sh <notes>` publishes it from a clean checkout at a pushed `v<version>` tag. The build number (`CFBundleVersion`) of both the Mac and iPhone apps is the Unix time of the commit they were built from (`git log -1 --format=%ct`), so one deploy's apps share it; the apps send it in their `Byre-Build` header and the Status tab's client list shows it. A deployed build's `CFBundleVersion` is at least the last release's, so the updater never offers to go back to it.
The appcast must never name an image that isn't available yet, and a published g.fc.run release must already have its attachments. If a failed release run leaves a draft on the releases page, delete it there before running again.
@@ -120,7 +122,7 @@ byre never signs with a key on the machine it runs on: every git command it runs
- **Orion.** Orion ignores key events that carry only a Unicode string, so typing posts each ASCII character as its key code. Some apps list `confirm` but ignore it, Orion's address bar among them; send Return instead. `do JavaScript` needs Orion's Develop → "Allow JavaScript from Apple Events".
- **Chrome.** Chrome refuses remote debugging on its default profile, so byre's Chrome is its own profile (`~/Library/Application Support/byre/chrome`), a separate Chrome beside any the human runs, and it keeps its logins: the human signs in there once. Chrome comes from `brew install --cask google-chrome`; macOS's "downloaded from the Internet" prompt on its first launch must be answered Open (or `osascript -e 'tell application "System Events" to tell process "CoreServicesUIAgent" to click button "Open" of window 1'`), and until it is, Chrome cannot accept debugging commands.
- **Chrome's trust boundary.** Use Chrome's loopback debugging port with byre's separate persistent profile and Chrome's default browser-origin checks; never add `--remote-allow-origins=*`. Other local OS accounts can reach that debugger and control its logged-in sessions. This is an accepted risk on the personal Macs byre runs on, as is access to the app's local preview forwards; do not add account-isolating brokers for these listeners. Chrome must be launched with `--remote-debugging-port=0` so it records its port and browser WebSocket path in the profile’s `DevToolsActivePort` file. After switching from the pipe or fixed-port transport, quit the old byre-profile Chrome once so the next tool call can start it with that discovery file. The saved profile and logins stay in place.
- **Permissions.** Agents' processes run as byre's descendants, so macOS privacy controls (TCC) hold byre responsible for them: the Screen Recording, Accessibility and Automation grants in System Settings → Privacy & Security are byre's, made by clicking at the Mac's screen, and each app scripted for the first time asks for its own Automation grant. Without Accessibility an untrusted process just sees no windows and no menu bar. Accessibility can list byre switched off; only its switch being on counts. TCC keys grants on the signing identifier and team, and every build is signed with Developer ID, so grants carry over from build to build. On a client Mac the grants belong to byre's app; `NSAppleEventsUsageDescription` in `mac/Info.plist` is what lets macOS ask for the Automation grant. `byre-hands check` prints the grants of the process that runs it; the Server and Settings tabs run it.
- **Permissions.** Agents' processes run as byre's descendants, so macOS privacy controls (TCC) hold byre responsible for them: the Screen Recording, Accessibility and Automation grants in System Settings → Privacy & Security are byre's, made by clicking at the Mac's screen, and each app scripted for the first time asks for its own Automation grant. Without Accessibility an untrusted process just sees no windows and no menu bar. Accessibility can list byre switched off; only its switch being on counts. TCC keys grants on the signing identifier and team, and every build is signed with Developer ID, so grants carry over from build to build. On a client Mac the grants belong to byre's app; `NSAppleEventsUsageDescription` in `mac/Info.plist` is what lets macOS ask for the Automation grant. The Desktop, Documents, Downloads and iCloud Drive folders each ask for their own Files & Folders grant the first time a process byre is responsible for reads them. `byre-hands check` prints the grants of the process that runs it, listing each of those folders so their prompts come up during the check rather than mid-turn; the Server and Settings tabs run it.
## Terminals
+18 -1
View File
@@ -56,9 +56,26 @@ func checkOrion() -> [String: String] {
}
}
func checkFolder(_ name: String, _ path: String) -> [String: String] {
let dir = (NSHomeDirectory() as NSString).appendingPathComponent(path)
if !FileManager.default.fileExists(atPath: dir) {
return permission(name, "unchecked", "\(dir) doesn't exist.", "FilesAndFolders")
}
do {
let entries = try FileManager.default.contentsOfDirectory(atPath: dir)
return permission(name, "granted", "Listed \(dir) (entries: \(entries.count)).", "FilesAndFolders")
} catch {
return permission(name, "missing", "Listing \(dir) failed (\(error.localizedDescription)). Switch \(name) on under byre in Files & Folders.", "FilesAndFolders")
}
}
func runCheck() {
Task.detached {
let rows = [await checkScreenRecording(), checkAccessibility(), checkOrion()]
let rows = [
await checkScreenRecording(), checkAccessibility(), checkOrion(),
checkFolder("Desktop Folder", "Desktop"), checkFolder("Documents Folder", "Documents"),
checkFolder("Downloads Folder", "Downloads"), checkFolder("iCloud Drive", "Library/Mobile Documents/com~apple~CloudDocs"),
]
let out = try! JSONSerialization.data(withJSONObject: rows, options: .withoutEscapingSlashes)
FileHandle.standardOutput.write(out + Data("\n".utf8))
exit(0)
+64 -2
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"log/slog"
"math"
"strings"
"time"
@@ -19,6 +20,13 @@ func (m *Manager) Fork(ctx context.Context, src api.Task, project api.Project, n
return t, err
}
func forkOrigin(t api.Task, from string) string {
if t.ForkedAt == nil {
return "continued from " + from
}
return fmt.Sprintf("forked from %s at snapshot %d", from, *t.ForkedAt)
}
func forkTitle(src api.Task, n int) string {
return fmt.Sprintf("%s (fork of #%d)", src.Title, n)
}
@@ -172,7 +180,7 @@ func (m *Manager) fork(ctx context.Context, src api.Task, project api.Project, n
return api.Task{}, "", fmt.Errorf("%w: snapshot %d of task %s has no recorded agent message at or before it to fork the conversation from", ErrConflict, n, src.Id)
}
t, err := m.createTask(ctx, project, api.Task{
return m.forkSession(ctx, project, api.Task{
Title: title,
MergeTitle: src.MergeTitle,
CreatedBy: creator.Login,
@@ -181,7 +189,61 @@ func (m *Manager) fork(ctx context.Context, src api.Task, project api.Project, n
ForkedAt: &n,
WorkflowStage: src.WorkflowStage,
ParentTaskId: src.ParentTaskId,
}, snap.CommitSha, creator.Login)
}, snap.CommitSha, creator, srcSess, at)
}
func (m *Manager) Continue(ctx context.Context, srcSess api.AgentSession, src api.Task, project api.Project, creator api.User, text, model, sendID, client string, images ...string) (api.Task, error) {
if creator.GitName == "" || creator.GitEmail == "" {
return api.Task{}, ErrNoGitIdentity
}
if src.Status == api.TaskStatusActive {
return api.Task{}, fmt.Errorf("%w: task %s is still active", ErrConflict, src.Id)
}
if srcSess.Role != api.Implementer {
return api.Task{}, fmt.Errorf("%w: session %s is a %s, not a task's agent", ErrConflict, srcSess.Id, srcSess.Role)
}
if srcSess.Vendor == api.Antigravity {
return api.Task{}, fmt.Errorf("%w: session %s ran on Antigravity, and agy can't fork a conversation", ErrConflict, srcSess.Id)
}
unlock := m.lockDelivery("continue:" + srcSess.Id)
defer unlock()
ctx = context.WithoutCancel(ctx)
prior, sent, err := m.priorSend(ctx, sendID)
if err != nil {
return api.Task{}, err
}
if sent {
return m.store.GetTask(ctx, prior.TaskId)
}
events, err := m.store.TaskEvents(ctx, src.Id, 0, -1)
if err != nil {
return api.Task{}, err
}
at := forkPoint(events, srcSess.Id, math.MaxInt64)
if at == "" {
return api.Task{}, fmt.Errorf("%w: session %s has no recorded agent message to continue the conversation from", ErrConflict, srcSess.Id)
}
t, sessionID, err := m.forkSession(ctx, project, api.Task{
Title: src.Title + " (continued)",
MergeTitle: src.MergeTitle,
CreatedBy: creator.Login,
ForkedFrom: &src.Id,
}, project.DefaultBranch, creator, srcSess, at)
if err != nil {
return api.Task{}, err
}
_, _, err = m.Message(ctx, sessionID, creator.Login, text, model, sendID, client, images...)
if err != nil {
return api.Task{}, fmt.Errorf("task %s created, but its agent did not start: %w", t.Id, err)
}
return t, nil
}
func (m *Manager) forkSession(ctx context.Context, project api.Project, task api.Task, base string, creator api.User, srcSess api.AgentSession, at string) (api.Task, string, error) {
t, err := m.createTask(ctx, project, task, base, creator.Login)
if err != nil {
return api.Task{}, "", err
}
+12
View File
@@ -142,6 +142,18 @@ func subtaskGuidance(parent api.Task) string {
}
func forkGuidance(task, src api.Task) string {
if task.ForkedAt == nil {
outcome := "was discarded, so none of its changes are in this worktree"
switch {
case src.MergeCommit != nil:
outcome = fmt.Sprintf("was merged as commit %s, so its work is already on the default branch", *src.MergeCommit)
case src.Status == api.TaskStatusMerged:
outcome = "was merged with no changes"
}
return fmt.Sprintf("\n- This conversation continues byre task %q, which %s. It goes on in a new task of its own: branch %s, worktree %s, started from the default branch. "+
"Everything before this point happened in %s, which no longer exists: from now on read and change files only in %s, and use paths under it.",
src.Title, outcome, task.Branch, task.WorktreePath, src.WorktreePath, task.WorktreePath)
}
return fmt.Sprintf("\n- This conversation was forked from byre task %q at its snapshot %d, into a task of its own: branch %s, worktree %s, which held that snapshot's files when the fork was made. "+
"Everything before the fork happened in %s, which belongs to the other task: from now on read and change files only in %s, never in the other worktree, and use paths under it.",
src.Title, *task.ForkedAt, task.Branch, task.WorktreePath, src.WorktreePath, task.WorktreePath)
+2 -2
View File
@@ -70,14 +70,14 @@ func (m *Manager) CheckPermissions(ctx context.Context) ([]api.Permission, error
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(ctx, 50*time.Second)
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
cmd := exec.CommandContext(ctx, bin, "check")
stderr := strings.Builder{}
cmd.Stderr = &stderr
out, err := cmd.Output()
if ctx.Err() == context.DeadlineExceeded {
return nil, fmt.Errorf("byre-hands check didn't finish within 50s; answer macOS's prompt on the server's Mac and check again")
return nil, fmt.Errorf("byre-hands check didn't finish within 2 minutes; answer macOS's prompts on the server's Mac and check again")
}
if err != nil {
return nil, fmt.Errorf("byre-hands check: %w: %s", err, strings.TrimSpace(stderr.String()))
+1 -1
View File
@@ -588,7 +588,7 @@ func (m *Manager) signalText(ctx context.Context, ev api.Event) (string, error)
return "", err
}
if task.ForkedFrom != nil {
return fmt.Sprintf("%s was forked by %s from %s at snapshot %d.", m.taskLabel(ctx, task.Id), ev.Actor, m.taskLabel(ctx, *task.ForkedFrom), *task.ForkedAt), nil
return fmt.Sprintf("%s was %s by %s.", m.taskLabel(ctx, task.Id), forkOrigin(task, m.taskLabel(ctx, *task.ForkedFrom)), ev.Actor), nil
}
if task.WorkflowStage != nil {
return fmt.Sprintf("%s was created to run stage %q of %s.", m.taskLabel(ctx, task.Id), *task.WorkflowStage, m.taskLabel(ctx, *task.ParentTaskId)), nil
+25 -38
View File
@@ -518,15 +518,18 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
}
m.mu.Lock()
working := m.paneFocus[user.Login].Focused || m.paneFocus[user.Login].Drafting
humanMoved := m.humanMoved[user.Login]
since := time.Since(humanMoved)
move := m.moveHolds[user.Login]
start := m.startHolds[user.Login]
since := time.Since(move.at)
moveHeld := since < moveHold
startHeld := !moveHeld && time.Since(m.startedTodo[user.Login]) < startHold
startHeld := !moveHeld && time.Since(start.at) < startHold
if moveHeld {
m.moveRefused[user.Login] = true
move.refused = true
m.moveHolds[user.Login] = move
}
if startHeld {
m.startRefused[user.Login] = true
start.refused = true
m.startHolds[user.Login] = start
}
m.mu.Unlock()
if moveHeld {
@@ -596,13 +599,12 @@ func (m *Manager) SetFocus(ctx context.Context, login string, f api.Focus, actor
m.mu.Lock()
m.paneMoved[login] = at
if actor == login {
m.humanMoved[login] = at
delete(m.moveRefused, login)
m.moveHolds[login] = showHold{at: at}
}
m.mu.Unlock()
if actor == login {
time.AfterFunc(moveHold, func() {
err := m.endMoveHold(context.Background(), login, at)
err := m.endShowHold(context.Background(), login, at, m.moveHolds, moveHoldEnded)
if err != nil {
slog.Error("end move hold", "login", login, "err", err)
}
@@ -702,21 +704,24 @@ const moveHold = 10 * time.Second
const moveHoldEnded = "The move hold is over: the human moved the right pane 10 seconds ago and hasn't moved it since, and a show of yours was refused in that time, so show works again under its other rules."
func (m *Manager) endMoveHold(ctx context.Context, login string, at time.Time) error {
type showHold struct {
at time.Time
refused bool
}
func (m *Manager) endShowHold(ctx context.Context, login string, at time.Time, holds map[string]showHold, text string) error {
m.mu.Lock()
current, ok := m.humanMoved[login]
if !ok || !current.Equal(at) {
hold := holds[login]
if !hold.at.Equal(at) {
m.mu.Unlock()
return nil
}
delete(m.humanMoved, login)
refused := m.moveRefused[login]
delete(m.moveRefused, login)
delete(holds, login)
m.mu.Unlock()
if !refused {
if !hold.refused {
return nil
}
return m.addLeadInput(ctx, store.LeadInput{Owner: login, Kind: store.LeadSignal, Actor: "byre", Text: moveHoldEnded})
return m.addLeadInput(ctx, store.LeadInput{Owner: login, Kind: store.LeadSignal, Actor: "byre", Text: text})
}
const startHold = 10 * time.Second
@@ -726,38 +731,20 @@ const startHoldEnded = "The start hold is over: the human started a TODO item 10
func (m *Manager) setStartedTodo(login string, started bool) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.startRefused, login)
if !started {
delete(m.startedTodo, login)
delete(m.startHolds, login)
return
}
at := time.Now()
m.startedTodo[login] = at
m.startHolds[login] = showHold{at: at}
time.AfterFunc(startHold, func() {
err := m.endStartHold(context.Background(), login, at)
err := m.endShowHold(context.Background(), login, at, m.startHolds, startHoldEnded)
if err != nil {
slog.Error("end start hold", "login", login, "err", err)
}
})
}
func (m *Manager) endStartHold(ctx context.Context, login string, at time.Time) error {
m.mu.Lock()
current, ok := m.startedTodo[login]
if !ok || !current.Equal(at) {
m.mu.Unlock()
return nil
}
delete(m.startedTodo, login)
refused := m.startRefused[login]
delete(m.startRefused, login)
m.mu.Unlock()
if !refused {
return nil
}
return m.addLeadInput(ctx, store.LeadInput{Owner: login, Kind: store.LeadSignal, Actor: "byre", Text: startHoldEnded})
}
func (m *Manager) modelOr(model string, l *live) string {
if model != "" {
return model
@@ -907,7 +894,7 @@ func (m *Manager) listTasksText(ctx context.Context) (string, error) {
fmt.Fprintf(b, "; from TODO item %q", *t.TodoItem)
}
if t.ForkedFrom != nil {
fmt.Fprintf(b, "; forked from %s at snapshot %d", *t.ForkedFrom, *t.ForkedAt)
fmt.Fprintf(b, "; %s", forkOrigin(t, *t.ForkedFrom))
}
if t.ParentTaskId != nil && t.WorkflowStage == nil {
fmt.Fprintf(b, "; subtask of %s", *t.ParentTaskId)
+4 -8
View File
@@ -58,10 +58,8 @@ type Manager struct {
paneFocus map[string]api.PaneFocus
paneReported map[string]time.Time
paneMoved map[string]time.Time
humanMoved map[string]time.Time
moveRefused map[string]bool
startedTodo map[string]time.Time
startRefused map[string]bool
moveHolds map[string]showHold
startHolds map[string]showHold
handoffModel map[string]string
mergeTurns map[string]mergeTurn
@@ -163,10 +161,8 @@ func New(st *store.Store, worktreeRoot, leadRoot, handsRoot, codexHome, antigrav
paneFocus: map[string]api.PaneFocus{},
paneReported: map[string]time.Time{},
paneMoved: map[string]time.Time{},
humanMoved: map[string]time.Time{},
moveRefused: map[string]bool{},
startedTodo: map[string]time.Time{},
startRefused: map[string]bool{},
moveHolds: map[string]showHold{},
startHolds: map[string]showHold{},
handoffModel: map[string]string{},
mergeTurns: map[string]mergeTurn{},
signals: make(chan []api.Event, 1024),
+2 -2
View File
@@ -34,8 +34,8 @@ func (m *Manager) mergeNote(ctx context.Context, task api.Task, login string) (s
if task.TodoItem != nil {
fmt.Fprintf(b, "Started from the TODO item %q.\n", *task.TodoItem)
}
if task.ForkedFrom != nil && task.ForkedAt != nil {
fmt.Fprintf(b, "Forked from task %s at snapshot %d.\n", *task.ForkedFrom, *task.ForkedAt)
if task.ForkedFrom != nil {
fmt.Fprintf(b, "This task was %s.\n", forkOrigin(task, "task "+*task.ForkedFrom))
}
fmt.Fprintf(b, "Full transcripts: the events of task %s in byre (GET /api/events?task_id=%s).\n", task.Id, task.Id)
+12 -20
View File
@@ -9,7 +9,6 @@ import (
"time"
"g.fc.run/ai/byre/internal/api"
"g.fc.run/ai/byre/internal/ids"
"g.fc.run/ai/byre/internal/mcp"
"g.fc.run/ai/byre/internal/schedule"
"g.fc.run/ai/byre/internal/store"
@@ -45,29 +44,22 @@ func (m *Manager) SaveSchedule(ctx context.Context, projectID, scheduleID string
sc.Id = existing.Id
sc.ProjectId = existing.ProjectId
sc.Owner = existing.Owner
sc, err = m.store.UpdateSchedule(ctx, sc, actor)
} else {
user, err := m.store.GetUser(ctx, login)
if err != nil {
return api.Schedule{}, err
}
m.poke()
return sc, nil
if user.GitName == "" || user.GitEmail == "" {
return api.Schedule{}, ErrNoGitIdentity
}
project, err := m.store.GetProject(ctx, projectID)
if err != nil {
return api.Schedule{}, err
}
sc.ProjectId = project.Id
sc.Owner = user.Login
}
user, err := m.store.GetUser(ctx, login)
if err != nil {
return api.Schedule{}, err
}
if user.GitName == "" || user.GitEmail == "" {
return api.Schedule{}, ErrNoGitIdentity
}
project, err := m.store.GetProject(ctx, projectID)
if err != nil {
return api.Schedule{}, err
}
sc.Id = ids.New(ids.Schedule)
sc.ProjectId = project.Id
sc.Owner = user.Login
sc, err = m.store.CreateSchedule(ctx, sc, actor)
sc, err = m.store.SaveSchedule(ctx, sc, actor)
if err != nil {
return api.Schedule{}, err
}
+11
View File
@@ -119,6 +119,17 @@ func (m *Manager) createTask(ctx context.Context, project api.Project, t api.Tas
t.Status = api.TaskStatusActive
t.CreatedAt = time.Now().UTC()
if base == project.DefaultBranch {
creator, err := m.store.GetUser(ctx, t.CreatedBy)
if err != nil {
return api.Task{}, err
}
err = git.EnsureBranchCommit(ctx, project.RepoPath, base, git.IdentityEnv(creator.GitName, creator.GitEmail))
if err != nil {
return api.Task{}, err
}
}
var err error
t.BaseRef, err = git.AddWorktree(ctx, project.RepoPath, t.WorktreePath, t.Branch, base)
if err != nil {
+391 -353
View File
@@ -1526,7 +1526,7 @@ type Task struct {
// Discarding Whether a Discard was started and the task isn't closed yet: its summary turn is waiting for the running turn, running, or done and the task is being removed.
Discarding bool `json:"discarding"`
// ForkedAt With forked_from: the n of the snapshot it was forked at.
// ForkedAt With forked_from: the n of the snapshot it was forked at; absent when it continues a closed task from its end.
ForkedAt *int `json:"forked_at,omitempty"`
// ForkedFrom The task this one was forked from; absent for other tasks.
@@ -2014,6 +2014,9 @@ type UpdateScheduleJSONRequestBody = ScheduleSave
// CompactSessionJSONRequestBody defines body for CompactSession for application/json ContentType.
type CompactSessionJSONRequestBody = CompactCreate
// ContinueSessionJSONRequestBody defines body for ContinueSession for application/json ContentType.
type ContinueSessionJSONRequestBody = MessageCreate
// SendMessageJSONRequestBody defines body for SendMessage for application/json ContentType.
type SendMessageJSONRequestBody = MessageCreate
@@ -2165,7 +2168,7 @@ type ServerInterface interface {
CreateSchedule(w http.ResponseWriter, r *http.Request, projectId string)
// (GET /api/projects/{projectId}/tasks)
ListTasks(w http.ResponseWriter, r *http.Request, projectId string)
ListActiveTasks(w http.ResponseWriter, r *http.Request, projectId string)
// (POST /api/projects/{projectId}/tasks)
CreateTask(w http.ResponseWriter, r *http.Request, projectId string)
@@ -2191,6 +2194,9 @@ type ServerInterface interface {
// (POST /api/sessions/{sessionId}/compact)
CompactSession(w http.ResponseWriter, r *http.Request, sessionId string)
// (POST /api/sessions/{sessionId}/continue)
ContinueSession(w http.ResponseWriter, r *http.Request, sessionId string)
// (POST /api/sessions/{sessionId}/interrupt)
InterruptSession(w http.ResponseWriter, r *http.Request, sessionId string)
@@ -3193,8 +3199,8 @@ func (siw *ServerInterfaceWrapper) CreateSchedule(w http.ResponseWriter, r *http
handler.ServeHTTP(w, r)
}
// ListTasks operation middleware
func (siw *ServerInterfaceWrapper) ListTasks(w http.ResponseWriter, r *http.Request) {
// ListActiveTasks operation middleware
func (siw *ServerInterfaceWrapper) ListActiveTasks(w http.ResponseWriter, r *http.Request) {
var err error
_ = err
@@ -3209,7 +3215,7 @@ func (siw *ServerInterfaceWrapper) ListTasks(w http.ResponseWriter, r *http.Requ
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.ListTasks(w, r, projectId)
siw.Handler.ListActiveTasks(w, r, projectId)
}))
for _, middleware := range siw.HandlerMiddlewares {
@@ -3436,6 +3442,32 @@ func (siw *ServerInterfaceWrapper) CompactSession(w http.ResponseWriter, r *http
handler.ServeHTTP(w, r)
}
// ContinueSession operation middleware
func (siw *ServerInterfaceWrapper) ContinueSession(w http.ResponseWriter, r *http.Request) {
var err error
_ = err
// ------------- Path parameter "sessionId" -------------
var sessionId string
err = runtime.BindStyledParameterWithOptions("simple", "sessionId", r.PathValue("sessionId"), &sessionId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "sessionId", Err: err})
return
}
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
siw.Handler.ContinueSession(w, r, sessionId)
}))
for _, middleware := range siw.HandlerMiddlewares {
handler = middleware(handler)
}
handler.ServeHTTP(w, r)
}
// InterruptSession operation middleware
func (siw *ServerInterfaceWrapper) InterruptSession(w http.ResponseWriter, r *http.Request) {
@@ -4724,7 +4756,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}", wrapper.GetProject)
m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/projects/{projectId}/review-workflow", wrapper.SetReviewWorkflow)
m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/projects/{projectId}/audit-workflow", wrapper.SetAuditWorkflow)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}/tasks", wrapper.ListTasks)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}/tasks", wrapper.ListActiveTasks)
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/projects/{projectId}/tasks", wrapper.CreateTask)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}/schedules", wrapper.ListSchedules)
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/projects/{projectId}/schedules", wrapper.CreateSchedule)
@@ -4758,6 +4790,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/reviews", wrapper.ListReviews)
m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/reviews/{reviewId}/findings/{index}", wrapper.SetFindingState)
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/tasks/{taskId}/snapshots/{n}/fork", wrapper.ForkSnapshot)
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/sessions/{sessionId}/continue", wrapper.ContinueSession)
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/sessions", wrapper.ListSessions)
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/tasks/{taskId}/sessions", wrapper.CreateSession)
m.HandleFunc(http.MethodDelete+" "+options.BaseURL+"/api/tasks/{taskId}/queue/{inputId}", wrapper.WithdrawMessage)
@@ -4796,354 +4829,359 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
// const string: with thousands of chunks the chained `+` fold is several
// times slower for the Go compiler than parsing a slice literal.
var swaggerSpec = []string{
"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",
"iEyh7ecrYTUicRMmooABvgNm8qtRUn/48P8PAA==",
}
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
+4 -17
View File
@@ -19,8 +19,6 @@ var clientMethods = []string{
"thread/start",
"thread/resume",
"thread/fork",
"thread/read",
"thread/compact/start",
"turn/start",
"turn/steer",
"turn/interrupt",
@@ -269,22 +267,11 @@ func (g *gen) generate(version string) []byte {
}
h.WriteString(")\n\n")
writeTable := func(name string, ms []method, field func(method) string) {
fmt.Fprintf(&h, "var %s = map[string]func() any{\n", name)
for _, m := range ms {
t := field(m)
if t == "" {
fmt.Fprintf(&h, "Method%s: nil,\n", pascal(m.name))
continue
}
fmt.Fprintf(&h, "Method%s: func() any { return &%s{} },\n", pascal(m.name), t)
}
h.WriteString("}\n\n")
h.WriteString("var notificationMethods = map[string]struct{}{\n")
for _, m := range notifications {
fmt.Fprintf(&h, "Method%s: {},\n", pascal(m.name))
}
writeTable("clientResults", client, func(m method) string { return m.response })
writeTable("notificationParams", notifications, func(m method) string { return m.params })
writeTable("serverRequestParams", requests, func(m method) string { return m.params })
writeTable("serverRequestResponses", requests, func(m method) string { return m.response })
h.WriteString("}\n\n")
h.Write(g.consts.Bytes())
h.Write(g.out.Bytes())
+101 -160
View File
@@ -11,8 +11,6 @@ const (
MethodThreadStart = "thread/start"
MethodThreadResume = "thread/resume"
MethodThreadFork = "thread/fork"
MethodThreadCompactStart = "thread/compact/start"
MethodThreadRead = "thread/read"
MethodTurnStart = "turn/start"
MethodTurnSteer = "turn/steer"
MethodTurnInterrupt = "turn/interrupt"
@@ -115,133 +113,92 @@ const (
MethodExecCommandApproval = "execCommandApproval"
)
var clientResults = map[string]func() any{
MethodInitialize: func() any { return &InitializeResponse{} },
MethodThreadStart: func() any { return &ThreadStartResponse{} },
MethodThreadResume: func() any { return &ThreadResumeResponse{} },
MethodThreadFork: func() any { return &ThreadForkResponse{} },
MethodThreadCompactStart: func() any { return &ThreadCompactStartResponse{} },
MethodThreadRead: func() any { return &ThreadReadResponse{} },
MethodTurnStart: func() any { return &TurnStartResponse{} },
MethodTurnSteer: func() any { return &TurnSteerResponse{} },
MethodTurnInterrupt: func() any { return &TurnInterruptResponse{} },
MethodAccountRateLimitsRead: func() any { return &GetAccountRateLimitsResponse{} },
}
var notificationParams = map[string]func() any{
MethodError: func() any { return &ErrorNotification{} },
MethodThreadStarted: func() any { return &ThreadStartedNotification{} },
MethodThreadStatusChanged: func() any { return &ThreadStatusChangedNotification{} },
MethodThreadReadStateChanged: func() any { return &ThreadReadStateChangedNotification{} },
MethodThreadArchived: func() any { return &ThreadArchivedNotification{} },
MethodThreadDeleted: func() any { return &ThreadDeletedNotification{} },
MethodThreadUnarchived: func() any { return &ThreadUnarchivedNotification{} },
MethodThreadClosed: func() any { return &ThreadClosedNotification{} },
MethodThreadReverted: func() any { return &ThreadRevertedNotification{} },
MethodSkillsChanged: func() any { return &SkillsChangedNotification{} },
MethodThreadNameUpdated: func() any { return &ThreadNameUpdatedNotification{} },
MethodThreadAttachmentUpdated: func() any { return &ThreadAttachmentUpdatedNotification{} },
MethodThreadGoalUpdated: func() any { return &ThreadGoalUpdatedNotification{} },
MethodThreadPredictionUpdated: func() any { return &ThreadPredictionUpdatedNotification{} },
MethodThreadGoalCleared: func() any { return &ThreadGoalClearedNotification{} },
MethodThreadQueueChanged: func() any { return &ThreadQueueChangedNotification{} },
MethodProjectChanged: func() any { return &ProjectChangedNotification{} },
MethodThreadProjectUpdated: func() any { return &ThreadProjectUpdatedNotification{} },
MethodThreadEnvironmentConnected: func() any { return &EnvironmentConnectionNotification{} },
MethodThreadEnvironmentDisconnected: func() any { return &EnvironmentConnectionNotification{} },
MethodThreadSettingsUpdated: func() any { return &ThreadSettingsUpdatedNotification{} },
MethodThreadTokenUsageUpdated: func() any { return &ThreadTokenUsageUpdatedNotification{} },
MethodTurnStarted: func() any { return &TurnStartedNotification{} },
MethodHookStarted: func() any { return &HookStartedNotification{} },
MethodTurnCompleted: func() any { return &TurnCompletedNotification{} },
MethodHookCompleted: func() any { return &HookCompletedNotification{} },
MethodTurnDiffUpdated: func() any { return &TurnDiffUpdatedNotification{} },
MethodTurnPlanUpdated: func() any { return &TurnPlanUpdatedNotification{} },
MethodItemStarted: func() any { return &ItemStartedNotification{} },
MethodItemAutoApprovalReviewStarted: func() any { return &ItemGuardianApprovalReviewStartedNotification{} },
MethodItemAutoApprovalReviewCompleted: func() any { return &ItemGuardianApprovalReviewCompletedNotification{} },
MethodAutoApprovalReviewStrictReviewRequired: func() any { return &StrictReviewRequiredNotification{} },
MethodItemCompleted: func() any { return &ItemCompletedNotification{} },
MethodItemAgentMessageDelta: func() any { return &AgentMessageDeltaNotification{} },
MethodItemPlanDelta: func() any { return &PlanDeltaNotification{} },
MethodCommandExecOutputDelta: func() any { return &CommandExecOutputDeltaNotification{} },
MethodProcessOutputDelta: func() any { return &ProcessOutputDeltaNotification{} },
MethodProcessExited: func() any { return &ProcessExitedNotification{} },
MethodItemCommandExecutionOutputDelta: func() any { return &CommandExecutionOutputDeltaNotification{} },
MethodItemCommandExecutionTerminalInteraction: func() any { return &TerminalInteractionNotification{} },
MethodItemFileChangeOutputDelta: func() any { return &FileChangeOutputDeltaNotification{} },
MethodItemFileChangePatchUpdated: func() any { return &FileChangePatchUpdatedNotification{} },
MethodServerRequestResolved: func() any { return &ServerRequestResolvedNotification{} },
MethodItemMcpToolCallProgress: func() any { return &McpToolCallProgressNotification{} },
MethodMcpServerOauthLoginCompleted: func() any { return &McpServerOauthLoginCompletedNotification{} },
MethodMcpServerStartupStatusUpdated: func() any { return &McpServerStatusUpdatedNotification{} },
MethodMcpServerEventStreamNotification: func() any { return &McpServerEventStreamNotification{} },
MethodAccountUpdated: func() any { return &AccountUpdatedNotification{} },
MethodAccountGatewayOAuthChanged: func() any { return &GatewayOAuthChangedNotification{} },
MethodAccountRateLimitsUpdated: func() any { return &AccountRateLimitsUpdatedNotification{} },
MethodAppListUpdated: func() any { return &AppListUpdatedNotification{} },
MethodRemoteControlStatusChanged: func() any { return &RemoteControlStatusChangedNotification{} },
MethodExternalAgentConfigImportProgress: func() any { return &ExternalAgentConfigImportProgressNotification{} },
MethodExternalAgentConfigImportCompleted: func() any { return &ExternalAgentConfigImportCompletedNotification{} },
MethodFsChanged: func() any { return &FsChangedNotification{} },
MethodItemReasoningSummaryTextDelta: func() any { return &ReasoningSummaryTextDeltaNotification{} },
MethodItemReasoningSummaryPartAdded: func() any { return &ReasoningSummaryPartAddedNotification{} },
MethodItemReasoningTextDelta: func() any { return &ReasoningTextDeltaNotification{} },
MethodThreadCompacted: func() any { return &ContextCompactedNotification{} },
MethodModelRerouted: func() any { return &ModelReroutedNotification{} },
MethodModelVerification: func() any { return &ModelVerificationNotification{} },
MethodModelProviderAuthRecoveryStarted: func() any { return &AuthRecoveryNotification{} },
MethodModelProviderAuthRecoveryCompleted: func() any { return &AuthRecoveryNotification{} },
MethodTurnModerationMetadata: func() any { return &TurnModerationMetadataNotification{} },
MethodModelSafetyBufferingUpdated: func() any { return &ModelSafetyBufferingUpdatedNotification{} },
MethodWarning: func() any { return &WarningNotification{} },
MethodGuardianWarning: func() any { return &GuardianWarningNotification{} },
MethodDeprecationNotice: func() any { return &DeprecationNoticeNotification{} },
MethodConfigWarning: func() any { return &ConfigWarningNotification{} },
MethodFuzzyFileSearchSessionUpdated: func() any { return &FuzzyFileSearchSessionUpdatedNotification{} },
MethodFuzzyFileSearchSessionCompleted: func() any { return &FuzzyFileSearchSessionCompletedNotification{} },
MethodThreadRealtimeStarted: func() any { return &ThreadRealtimeStartedNotification{} },
MethodThreadRealtimeItemAdded: func() any { return &ThreadRealtimeItemAddedNotification{} },
MethodThreadRealtimeItemStarted: func() any { return &ThreadRealtimeItemStartedNotification{} },
MethodThreadRealtimeItemTranscriptDelta: func() any { return &ThreadRealtimeItemTranscriptDeltaNotification{} },
MethodThreadRealtimeItemCompleted: func() any { return &ThreadRealtimeItemCompletedNotification{} },
MethodThreadRealtimeTranscriptDelta: func() any { return &ThreadRealtimeTranscriptDeltaNotification{} },
MethodThreadRealtimeTranscriptDone: func() any { return &ThreadRealtimeTranscriptDoneNotification{} },
MethodThreadRealtimeOutputAudioDelta: func() any { return &ThreadRealtimeOutputAudioDeltaNotification{} },
MethodThreadRealtimeSdp: func() any { return &ThreadRealtimeSdpNotification{} },
MethodThreadRealtimeError: func() any { return &ThreadRealtimeErrorNotification{} },
MethodThreadRealtimeClosed: func() any { return &ThreadRealtimeClosedNotification{} },
MethodWindowsWorldWritableWarning: func() any { return &WindowsWorldWritableWarningNotification{} },
MethodWindowsSandboxSetupCompleted: func() any { return &WindowsSandboxSetupCompletedNotification{} },
MethodAccountLoginCompleted: func() any { return &AccountLoginCompletedNotification{} },
}
var serverRequestParams = map[string]func() any{
MethodItemCommandExecutionRequestApproval: func() any { return &CommandExecutionRequestApprovalParams{} },
MethodItemFileChangeRequestApproval: func() any { return &FileChangeRequestApprovalParams{} },
MethodItemToolRequestUserInput: func() any { return &ToolRequestUserInputParams{} },
MethodMcpServerElicitationRequest: func() any { return &McpServerElicitationRequestParams{} },
MethodItemPermissionsRequestApproval: func() any { return &PermissionsRequestApprovalParams{} },
MethodItemToolCall: func() any { return &DynamicToolCallParams{} },
MethodAccountChatgptAuthTokensRefresh: func() any { return &ChatgptAuthTokensRefreshParams{} },
MethodAttestationGenerate: func() any { return &AttestationGenerateParams{} },
MethodCurrentTimeRead: func() any { return &CurrentTimeReadParams{} },
MethodApplyPatchApproval: func() any { return &ApplyPatchApprovalParams{} },
MethodExecCommandApproval: func() any { return &ExecCommandApprovalParams{} },
}
var serverRequestResponses = map[string]func() any{
MethodItemCommandExecutionRequestApproval: func() any { return &CommandExecutionRequestApprovalResponse{} },
MethodItemFileChangeRequestApproval: func() any { return &FileChangeRequestApprovalResponse{} },
MethodItemToolRequestUserInput: func() any { return &ToolRequestUserInputResponse{} },
MethodMcpServerElicitationRequest: func() any { return &McpServerElicitationRequestResponse{} },
MethodItemPermissionsRequestApproval: func() any { return &PermissionsRequestApprovalResponse{} },
MethodItemToolCall: func() any { return &DynamicToolCallResponse{} },
MethodAccountChatgptAuthTokensRefresh: func() any { return &ChatgptAuthTokensRefreshResponse{} },
MethodAttestationGenerate: func() any { return &AttestationGenerateResponse{} },
MethodCurrentTimeRead: func() any { return &CurrentTimeReadResponse{} },
MethodApplyPatchApproval: func() any { return &ApplyPatchApprovalResponse{} },
MethodExecCommandApproval: func() any { return &ExecCommandApprovalResponse{} },
var notificationMethods = map[string]struct{}{
MethodError: {},
MethodThreadStarted: {},
MethodThreadStatusChanged: {},
MethodThreadReadStateChanged: {},
MethodThreadArchived: {},
MethodThreadDeleted: {},
MethodThreadUnarchived: {},
MethodThreadClosed: {},
MethodThreadReverted: {},
MethodSkillsChanged: {},
MethodThreadNameUpdated: {},
MethodThreadAttachmentUpdated: {},
MethodThreadGoalUpdated: {},
MethodThreadPredictionUpdated: {},
MethodThreadGoalCleared: {},
MethodThreadQueueChanged: {},
MethodProjectChanged: {},
MethodThreadProjectUpdated: {},
MethodThreadEnvironmentConnected: {},
MethodThreadEnvironmentDisconnected: {},
MethodThreadSettingsUpdated: {},
MethodThreadTokenUsageUpdated: {},
MethodTurnStarted: {},
MethodHookStarted: {},
MethodTurnCompleted: {},
MethodHookCompleted: {},
MethodTurnDiffUpdated: {},
MethodTurnPlanUpdated: {},
MethodItemStarted: {},
MethodItemAutoApprovalReviewStarted: {},
MethodItemAutoApprovalReviewCompleted: {},
MethodAutoApprovalReviewStrictReviewRequired: {},
MethodItemCompleted: {},
MethodItemAgentMessageDelta: {},
MethodItemPlanDelta: {},
MethodCommandExecOutputDelta: {},
MethodProcessOutputDelta: {},
MethodProcessExited: {},
MethodItemCommandExecutionOutputDelta: {},
MethodItemCommandExecutionTerminalInteraction: {},
MethodItemFileChangeOutputDelta: {},
MethodItemFileChangePatchUpdated: {},
MethodServerRequestResolved: {},
MethodItemMcpToolCallProgress: {},
MethodMcpServerOauthLoginCompleted: {},
MethodMcpServerStartupStatusUpdated: {},
MethodMcpServerEventStreamNotification: {},
MethodAccountUpdated: {},
MethodAccountGatewayOAuthChanged: {},
MethodAccountRateLimitsUpdated: {},
MethodAppListUpdated: {},
MethodRemoteControlStatusChanged: {},
MethodExternalAgentConfigImportProgress: {},
MethodExternalAgentConfigImportCompleted: {},
MethodFsChanged: {},
MethodItemReasoningSummaryTextDelta: {},
MethodItemReasoningSummaryPartAdded: {},
MethodItemReasoningTextDelta: {},
MethodThreadCompacted: {},
MethodModelRerouted: {},
MethodModelVerification: {},
MethodModelProviderAuthRecoveryStarted: {},
MethodModelProviderAuthRecoveryCompleted: {},
MethodTurnModerationMetadata: {},
MethodModelSafetyBufferingUpdated: {},
MethodWarning: {},
MethodGuardianWarning: {},
MethodDeprecationNotice: {},
MethodConfigWarning: {},
MethodFuzzyFileSearchSessionUpdated: {},
MethodFuzzyFileSearchSessionCompleted: {},
MethodThreadRealtimeStarted: {},
MethodThreadRealtimeItemAdded: {},
MethodThreadRealtimeItemStarted: {},
MethodThreadRealtimeItemTranscriptDelta: {},
MethodThreadRealtimeItemCompleted: {},
MethodThreadRealtimeTranscriptDelta: {},
MethodThreadRealtimeTranscriptDone: {},
MethodThreadRealtimeOutputAudioDelta: {},
MethodThreadRealtimeSdp: {},
MethodThreadRealtimeError: {},
MethodThreadRealtimeClosed: {},
MethodWindowsWorldWritableWarning: {},
MethodWindowsSandboxSetupCompleted: {},
MethodAccountLoginCompleted: {},
}
const (
@@ -524,11 +481,6 @@ const (
ModeKindDefault ModeKind = "default"
)
const (
ThreadUnreadPositionTypeThreadStart = "threadStart"
ThreadUnreadPositionTypeTurn = "turn"
)
const (
AdditionalContextKindUntrusted AdditionalContextKind = "untrusted"
AdditionalContextKindApplication AdditionalContextKind = "application"
@@ -588,6 +540,11 @@ const (
RateLimitReachedTypeWorkspaceMemberUsageLimitReached RateLimitReachedType = "workspace_member_usage_limit_reached"
)
const (
ThreadUnreadPositionTypeThreadStart = "threadStart"
ThreadUnreadPositionTypeTurn = "turn"
)
const (
ThreadAttachmentOperationCreated ThreadAttachmentOperation = "created"
ThreadAttachmentOperationDeleted ThreadAttachmentOperation = "deleted"
@@ -1562,32 +1519,6 @@ type ThreadForkResponse struct {
Thread Thread `json:"thread"`
}
type ThreadCompactStartParams struct {
ThreadID string `json:"threadId"`
}
type ThreadCompactStartResponse = map[string]any
type ThreadReadParams struct {
IncludeTurns bool `json:"includeTurns,omitempty"`
ThreadID string `json:"threadId"`
}
type ThreadUnreadPosition struct {
TurnID string `json:"turnId,omitempty"`
Type string `json:"type"`
}
type ThreadReadState struct {
FirstUnread *ThreadUnreadPosition `json:"firstUnread,omitempty"`
Revision string `json:"revision"`
}
type ThreadReadResponse struct {
ReadState *ThreadReadState `json:"readState,omitempty"`
Thread Thread `json:"thread"`
}
type AdditionalContextKind string
type AdditionalContextEntry struct {
@@ -1745,6 +1676,16 @@ type ThreadStatusChangedNotification struct {
ThreadID string `json:"threadId"`
}
type ThreadUnreadPosition struct {
TurnID string `json:"turnId,omitempty"`
Type string `json:"type"`
}
type ThreadReadState struct {
FirstUnread *ThreadUnreadPosition `json:"firstUnread,omitempty"`
Revision string `json:"revision"`
}
type ThreadReadStateChangedNotification struct {
ReadState ThreadReadState `json:"readState"`
ThreadID string `json:"threadId"`
+5 -46
View File
@@ -41,22 +41,6 @@ type Event struct {
Read time.Time
}
func (e Event) Decode() (any, error) {
ctor, ok := notificationParams[e.Method]
if !ok {
return nil, fmt.Errorf("unknown notification %q", e.Method)
}
if ctor == nil {
return nil, nil
}
v := ctor()
err := json.Unmarshal(e.Params, v)
if err != nil {
return nil, fmt.Errorf("decode %s: %w", e.Method, err)
}
return v, nil
}
type RPCError struct {
Code int64 `json:"code"`
Message string `json:"message"`
@@ -92,11 +76,10 @@ type Session struct {
pendingMu sync.Mutex
pending map[int64]chan rpcResult
stateMu sync.Mutex
threadID string
turnID string
model string
userAgent string
stateMu sync.Mutex
threadID string
turnID string
model string
servers map[string]*mcp.Server
reminder string
@@ -264,10 +247,6 @@ func (s *Session) initialize(ctx context.Context) error {
return fmt.Errorf("initialize: %w", err)
}
s.stateMu.Lock()
s.userAgent = init.UserAgent
s.stateMu.Unlock()
return s.notify("initialized", nil)
}
@@ -314,10 +293,6 @@ func (s *Session) Events() <-chan Event {
return s.events
}
func (s *Session) Done() <-chan struct{} {
return s.done
}
func (s *Session) Pid() int {
return s.cmd.Process.Pid
}
@@ -341,18 +316,6 @@ func (s *Session) ID() string {
return s.threadID
}
func (s *Session) TurnID() string {
s.stateMu.Lock()
defer s.stateMu.Unlock()
return s.turnID
}
func (s *Session) UserAgent() string {
s.stateMu.Lock()
defer s.stateMu.Unlock()
return s.userAgent
}
func (s *Session) SetModel(model string) {
s.stateMu.Lock()
s.model = model
@@ -417,10 +380,6 @@ func (s *Session) Steer(text string) (*Reply, error) {
return s.request(context.Background(), MethodTurnSteer, params)
}
func (s *Session) Compact(ctx context.Context) error {
return s.Call(ctx, MethodThreadCompactStart, ThreadCompactStartParams{ThreadID: s.ID()}, nil)
}
const rpcTimeout = time.Minute
type Reply struct {
@@ -603,7 +562,7 @@ func (s *Session) handleLine(line []byte) {
return
}
if _, ok := notificationParams[m.Method]; !ok {
if _, ok := notificationMethods[m.Method]; !ok {
s.badLine(line, fmt.Sprintf("unrecognized method %q", m.Method))
}
+28
View File
@@ -75,6 +75,34 @@ func AddWorktree(ctx context.Context, repo, path, branch, base string) (string,
return sha, nil
}
func EnsureBranchCommit(ctx context.Context, repo, branch string, env []string) error {
_, _, code, err := runCode(ctx, repo, nil, "rev-parse", "--verify", "--quiet", "refs/heads/"+branch+"^{commit}")
if err != nil || code == 0 {
return err
}
refs, err := run(ctx, repo, "for-each-ref", "--count=1")
if err != nil || refs != "" {
return err
}
tree, err := run(ctx, repo, "mktree")
if err != nil {
return err
}
commit, err := runEnv(ctx, repo, env, "commit-tree", tree, "-m", "Initial commit")
if err != nil {
return err
}
_, stderr, code, err := runNetwork(ctx, repo, "push", Remote, commit+":refs/heads/"+branch)
if err != nil {
return err
}
if code != 0 {
return fmt.Errorf("pushing %s to %s: exit status %d: %s", branch, Remote, code, stderr)
}
_, err = run(ctx, repo, "update-ref", "refs/heads/"+branch, commit, "")
return err
}
func AddDetachedWorktree(ctx context.Context, repo, path, commit string) error {
_, err := run(ctx, repo, "worktree", "add", "--detach", path, commit)
return err
+56 -2
View File
@@ -345,14 +345,14 @@ func (s *Server) GetProject(w http.ResponseWriter, r *http.Request, projectID st
writeJSON(w, http.StatusOK, p)
}
func (s *Server) ListTasks(w http.ResponseWriter, r *http.Request, projectID string) {
func (s *Server) ListActiveTasks(w http.ResponseWriter, r *http.Request, projectID string) {
_, err := s.store.GetProject(r.Context(), projectID)
if err != nil {
writeStoreError(w, err)
return
}
tasks, err := s.store.ListTasks(r.Context(), projectID)
tasks, err := s.store.ListActiveTasks(r.Context(), projectID)
if err != nil {
writeStoreError(w, err)
return
@@ -840,6 +840,60 @@ func (s *Server) ForkSnapshot(w http.ResponseWriter, r *http.Request, taskID str
writeJSON(w, http.StatusCreated, fork)
}
func (s *Server) ContinueSession(w http.ResponseWriter, r *http.Request, sessionID string) {
req := api.MessageCreate{}
if !readJSON(w, r, &req) {
return
}
images, ok := s.images(w, r, req.Images)
if !ok {
return
}
if strings.TrimSpace(req.Text) == "" && len(images) == 0 {
writeError(w, http.StatusBadRequest, errors.New("text or images are required"))
return
}
model := ""
if req.Model != nil {
model = *req.Model
}
sendID := ""
if req.SendId != nil {
sendID = *req.SendId
}
src, err := s.store.GetSession(r.Context(), sessionID)
if err != nil {
writeStoreError(w, err)
return
}
t, err := s.store.GetTask(r.Context(), src.TaskId)
if err != nil {
writeStoreError(w, err)
return
}
p, err := s.store.GetProject(r.Context(), t.ProjectId)
if err != nil {
writeStoreError(w, err)
return
}
u, err := s.store.GetUser(r.Context(), actor(r))
if err != nil {
writeStoreError(w, err)
return
}
task, err := s.agents.Continue(r.Context(), src, t, p, u, req.Text, model, sendID, r.Header.Get("Byre-Client"), images...)
if err != nil {
writeAgentError(w, err)
return
}
writeJSON(w, http.StatusCreated, task)
}
func (s *Server) ListSessions(w http.ResponseWriter, r *http.Request, taskID string) {
_, err := s.store.GetTask(r.Context(), taskID)
if err != nil {
+16 -23
View File
@@ -9,6 +9,7 @@ import (
"time"
"g.fc.run/ai/byre/internal/api"
"g.fc.run/ai/byre/internal/ids"
"g.fc.run/ai/byre/internal/schedule"
)
@@ -89,32 +90,24 @@ func writeWeekdays(ctx context.Context, tx *sql.Tx, sc api.Schedule) error {
return nil
}
func (s *Store) CreateSchedule(ctx context.Context, sc api.Schedule, actor string) (api.Schedule, error) {
func (s *Store) SaveSchedule(ctx context.Context, sc api.Schedule, actor string) (api.Schedule, error) {
sc.NextRun = schedule.Next(sc, time.Now())
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
_, err := tx.ExecContext(ctx, `INSERT INTO schedules (`+scheduleColumns+`, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
sc.Id, sc.ProjectId, sc.Owner, sc.Title, string(sc.Every), sc.Hour, sc.Minute, sc.DayOfMonth, sc.Prompt, sc.Model, sc.Workflow, formatTime(sc.NextRun), now())
if err != nil {
return err
if sc.Id == "" {
sc.Id = ids.New(ids.Schedule)
_, err := tx.ExecContext(ctx, `INSERT INTO schedules (`+scheduleColumns+`, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
sc.Id, sc.ProjectId, sc.Owner, sc.Title, string(sc.Every), sc.Hour, sc.Minute, sc.DayOfMonth, sc.Prompt, sc.Model, sc.Workflow, formatTime(sc.NextRun), now())
if err != nil {
return err
}
} else {
_, err := tx.ExecContext(ctx, `UPDATE schedules SET title = ?, every = ?, hour = ?, minute = ?, day_of_month = ?, prompt = ?, model = ?, workflow = ?, next_run_at = ? WHERE id = ?`,
sc.Title, string(sc.Every), sc.Hour, sc.Minute, sc.DayOfMonth, sc.Prompt, sc.Model, sc.Workflow, formatTime(sc.NextRun), sc.Id)
if err != nil {
return err
}
}
err = writeWeekdays(ctx, tx, sc)
if err != nil {
return err
}
return emit(api.Event{Kind: "schedule.saved", ProjectId: &sc.ProjectId, Actor: actor, Payload: ToPayload(sc)})
})
return sc, err
}
func (s *Store) UpdateSchedule(ctx context.Context, sc api.Schedule, actor string) (api.Schedule, error) {
sc.NextRun = schedule.Next(sc, time.Now())
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
_, err := tx.ExecContext(ctx, `UPDATE schedules SET title = ?, every = ?, hour = ?, minute = ?, day_of_month = ?, prompt = ?, model = ?, workflow = ?, next_run_at = ? WHERE id = ?`,
sc.Title, string(sc.Every), sc.Hour, sc.Minute, sc.DayOfMonth, sc.Prompt, sc.Model, sc.Workflow, formatTime(sc.NextRun), sc.Id)
if err != nil {
return err
}
err = writeWeekdays(ctx, tx, sc)
err := writeWeekdays(ctx, tx, sc)
if err != nil {
return err
}
+8 -2
View File
@@ -410,6 +410,10 @@ func (s *Store) ListTasks(ctx context.Context, projectID string) ([]api.Task, er
return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE project_id = ? ORDER BY created_at DESC`, projectID)
}
func (s *Store) ListActiveTasks(ctx context.Context, projectID string) ([]api.Task, error) {
return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE project_id = ? AND status = ? ORDER BY created_at DESC`, projectID, string(api.TaskStatusActive))
}
func (s *Store) GetTask(ctx context.Context, id string) (api.Task, error) {
t, err := scanTask(s.db.QueryRowContext(ctx, `SELECT `+taskColumns+` FROM tasks WHERE id = ?`, id))
if errors.Is(err, sql.ErrNoRows) {
@@ -958,8 +962,8 @@ func (s *Store) TranscriptEvents(ctx context.Context, taskID string, after int64
cut := int64(0)
err = s.db.QueryRowContext(ctx, `
SELECT coalesce(max(seq), 0) FROM events
WHERE task_id = ? AND kind = 'snapshot.created' AND json_extract(payload, '$.n') = ?
`, from, at).Scan(&cut)
WHERE task_id = ? AND (? = 0 OR kind = 'snapshot.created' AND json_extract(payload, '$.n') = ?)
`, from, at, at).Scan(&cut)
if err != nil {
return nil, err
}
@@ -1750,6 +1754,8 @@ func scanTask(row scanner) (api.Task, error) {
}
if forkedFrom != "" {
t.ForkedFrom = &forkedFrom
}
if forkedAt != 0 {
t.ForkedAt = &forkedAt
}
if mergeQueuedAt != "" {
+6
View File
@@ -41,5 +41,11 @@
</dict>
<key>NSAppleEventsUsageDescription</key>
<string>byre's agents drive Orion on this Mac through AppleScript when a task asks them to.</string>
<key>NSDesktopFolderUsageDescription</key>
<string>byre's agents read and write files on your Desktop when a task asks them to.</string>
<key>NSDocumentsFolderUsageDescription</key>
<string>byre's agents read and write files in Documents when a task asks them to.</string>
<key>NSDownloadsFolderUsageDescription</key>
<string>byre's agents read and write files in Downloads when a task asks them to.</string>
</dict>
</plist>
+7 -2
View File
@@ -4,6 +4,9 @@ import HTTPTypes
import OpenAPIRuntime
import OpenAPIURLSession
import os
#if os(iOS)
import UIKit
#endif
let log = Logger(subsystem: "run.fc.byre.mac", category: "byre")
@@ -104,7 +107,8 @@ private let clientID = newID("cli")
#if os(macOS)
let deviceName = Host.current().localizedName ?? ProcessInfo.processInfo.hostName
#else
let deviceName = deviceKind
let deviceNameKey = "deviceName"
let deviceName = UserDefaults.standard.string(forKey: deviceNameKey) ?? deviceKind
#endif
final class API: Sendable {
@@ -112,9 +116,10 @@ final class API: Sendable {
let session: URLSession
let client: Client
init(origin: URL) {
init(origin: URL, timeout: TimeInterval = 60) {
self.origin = origin
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = timeout
config.httpAdditionalHeaders = [
"Byre-Build": Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String,
"Byre-Client": clientID,
+1 -1
View File
@@ -104,7 +104,7 @@ struct ByreCommands: Commands {
PasteboardCommands()
CommandMenu("Go") {
goButton("Switch Task", "k")
goButton("Project", "p")
goButton("Task List", "p")
goButton("Lead", "l")
goButton("Merge", "s")
serverButton("Previous Server", "[", -1)
+53
View File
@@ -6,6 +6,26 @@ enum ChatKind: Equatable {
case task(String?)
}
#if os(macOS)
private let leadPlaces = "Tasks open on the right; [switch between them](byre-app://switcher) with ⌘K. Their questions, finished work and reviews wait for you there."
private let leadSettings = "pick the default model for new tasks, turn on notifications, and check this Mac's permissions so agents can see and control it and read your Desktop, Documents and Downloads. The [Server](byre-app://tab?name=server) tab checks the same for the server's agents."
#else
private let leadPlaces = "Tasks live in the Tasks tab, with the ones waiting on you marked; [find or start one](byre-app://switcher) any time."
private let leadSettings = "pick the default model for new tasks, turn on notifications, and choose this server's window color."
#endif
private let leadMore = "As you work, the lead keeps [Notes](byre-app://tab?name=notes) on how you like things done, [Workflows](byre-app://tab?name=workflows) run several agents on one job, such as a review, and [Status](byre-app://tab?name=status) shows the turns running now and how much of each vendor's limits you've used."
private let leadWelcome = """
**Welcome to byre.** This is your lead, the agent you talk to about all your work. Tell it what you want done and it starts tasks: each one is an agent working on its own branch of a project, which you merge when you're happy with it. Ask it to add a project first, giving the path of a git repository on the server.
\(leadPlaces)
\(leadMore)
Start in [Settings](byre-app://tab?name=settings): \(leadSettings)
"""
struct AttentionGroup: Identifiable {
let id: String
let label: String
@@ -114,7 +134,17 @@ final class ChatModel {
member != nil
}
var closed: Bool {
guard let taskId, let task = store.findTask(taskId) else {
return false
}
return task.status != .active
}
var placeholder: String {
if closed {
return "Continue in a new task, started from main, whose agent picks up this conversation. Paste or drop images to attach them."
}
if team {
return "Post to the team's board; every agent of the team gets it."
}
@@ -157,6 +187,10 @@ final class ChatModel {
kind == .lead && store.leadHasOlder()
}
var welcome: Bool {
kind == .lead && !loading && !hasOlder && builder.root.rows.isEmpty && builder.pending.isEmpty
}
func start() {
if started {
return
@@ -444,6 +478,14 @@ final class ChatModel {
case .lead:
_ = try await api.messageLead(body: .json(.init(text: text, images: images, model: sessionId != nil ? model : firstModel, sendId: sendID))).accepted
case .task(let id):
if closed {
guard let sessionId else {
throw ApiError(status: 0, message: "This task has no agent conversation to continue.")
}
let task = try await api.continueSession(path: .init(sessionId: sessionId), body: .json(.init(text: text, images: images, model: team ? nil : model, sendId: sendID))).created.body.json
store.focusTask(task.id)
return
}
if team {
if images != nil {
throw ApiError(status: 0, message: "The board takes text only; drop the images to post.")
@@ -790,6 +832,10 @@ private struct TranscriptScroll: View {
PendingView(pending: p, model: model)
.padding(.top, 12)
}
if model.welcome {
MarkdownView(blocks: parseMarkdown(leadWelcome))
.padding(.top, 12)
}
}
}
.scrollTargetLayout()
@@ -1448,6 +1494,7 @@ private struct ReadyBubble: View {
if current, let task {
let busy = store.taskBusy(task.id)
let commented = comments(task).count
let other = !trimmed(store.drafts?.textAnswer(draftKey(task)) ?? "").isEmpty || store.images.has(draftKey(task))
AttentionReply(key: draftKey(task), placeholder: "Something else to tell the agent; sending it dismisses this", busy: dismissing, focus: $focus, send: { send(task) }) {
Button("Dismiss") {
dismiss(task, then: nil)
@@ -1462,6 +1509,12 @@ private struct ReadyBubble: View {
}
.buttonStyle(WebButtonStyle(primary: true))
.help("Send your comments to the agent in one message; sending dismisses this")
} else if other {
Button("Send") {
send(task)
}
.buttonStyle(WebButtonStyle(primary: true))
.help("Send this to the agent; sending dismisses this")
} else if !discard {
MergeButton(task: task, busy: busy, unmerged: unmerged, keepOpen: true, primary: task.readyMerge == ._continue)
MergeButton(task: task, busy: busy, unmerged: unmerged, keepOpen: false, primary: task.readyMerge != ._continue)
+6 -5
View File
@@ -482,7 +482,7 @@ struct ComposerView: View {
}
private func modelState() -> ModelState {
let live = chat.liveSession
let live = chat.closed ? chat.session : chat.liveSession
let current = live.map { $0.model.isEmpty ? chat.builder.context[$0.id]?.model ?? "" : $0.model } ?? ""
let offered = live.map { s in models.filter { vendorOf($0) == s.vendor.rawValue } } ?? models
let options = !current.isEmpty && !offered.contains(current) ? [current] + offered : offered
@@ -632,12 +632,13 @@ struct ComposerView: View {
.buttonStyle(WebButtonStyle(danger: true))
.disabled(!running)
.fixedSize()
Button("Send") {
Button(chat.closed ? "Continue" : "Send") {
submit(modelState())
}
.buttonStyle(WebButtonStyle())
.disabled(!shown)
.fixedSize()
.help(chat.closed ? "Start a new task from main whose agent continues this conversation with your message" : "Send the message")
}
}
@@ -668,7 +669,7 @@ struct ComposerView: View {
text = ""
let sent = drafts.startSend(draftKey, value, imgs.count)
let sendID = newID("snd")
let cancel = chat.team ? { true } : chat.builder.showPending(value, imgs, sendID)
let cancel = chat.team || chat.closed ? { true } : chat.builder.showPending(value, imgs, sendID)
let sessionId = m.live?.id
Task {
var ids: [String]? = nil
@@ -686,7 +687,7 @@ struct ComposerView: View {
return
} catch {
let refused = (rootError(error) as? ApiError).map { $0.status < 500 } ?? false
if chat.team || refused {
if (chat.team && !chat.closed) || refused {
guard cancel() else {
log.notice("send failed after the server confirmed it: \(errorText(error), privacy: .public)")
drafts.endSend(sent, true)
@@ -698,7 +699,7 @@ struct ComposerView: View {
overlays.showAlert(errorText(error))
return
}
guard chat.builder.unconfirmed(sendID) else {
guard chat.closed || chat.builder.unconfirmed(sendID) else {
drafts.endSend(sent, true)
return
}
+3 -1
View File
@@ -34,7 +34,7 @@ struct OverlayAlert: Identifiable {
let content: AnyView
}
enum LeftTab {
enum LeftTab: String {
case lead, status, notes, workflows, server, settings
}
@@ -51,7 +51,9 @@ final class Overlays {
var switcher: SwitcherModel?
var leftTab = LeftTab.lead
var leadFocus = 0
var tabFocus = 0
var projectFocus = false
var taskList = false
@ObservationIgnored var pickerFrame: CGRect = .zero
@ObservationIgnored var menuHover = false
var paneFrame: CGRect = .zero
+15 -10
View File
@@ -435,16 +435,21 @@ private struct WebButton: View {
var body: some View {
let fill = danger ? Palette.danger : Palette.accent
configuration.label
.font(Fonts.font(13, weight: primary ? 700 : 400))
.foregroundStyle(primary ? Palette.bg : danger ? Palette.danger : Palette.text)
.padding(.horizontal, 12)
.padding(.vertical, touchOnly ? 7 : 4)
.background(primary ? fill : Palette.raised, in: RoundedRectangle(cornerRadius: 6))
.overlay(RoundedRectangle(cornerRadius: 6).stroke(primary ? fill : hover && enabled ? Palette.muted : Palette.border))
.opacity(enabled ? (configuration.isPressed ? 0.8 : 1) : 0.45)
.contentShape(Rectangle())
.onHover { hover = $0 }
ZStack {
configuration.label
.font(Fonts.font(13, weight: 700))
.hidden()
configuration.label
.font(Fonts.font(13, weight: primary ? 700 : 400))
}
.foregroundStyle(primary ? Palette.bg : danger ? Palette.danger : Palette.text)
.padding(.horizontal, 12)
.padding(.vertical, touchOnly ? 7 : 4)
.background(primary ? fill : Palette.raised, in: RoundedRectangle(cornerRadius: 6))
.overlay(RoundedRectangle(cornerRadius: 6).stroke(primary ? fill : hover && enabled ? Palette.muted : Palette.border))
.opacity(enabled ? (configuration.isPressed ? 0.8 : 1) : 0.45)
.contentShape(Rectangle())
.onHover { hover = $0 }
}
}
+1 -1
View File
@@ -181,7 +181,7 @@ private final class Forward: @unchecked Sendable {
}
}
private final class Splice: @unchecked Sendable {
final class Splice: @unchecked Sendable {
private let conn: NWConnection
private let fd: Int32
private let io: DispatchIO
+16
View File
@@ -66,6 +66,11 @@ func openAppLink(_ url: URL, _ store: Store, _ overlays: Overlays) -> OpenURLAct
let taskId = q["task"] ?? ""
store.route("#/task/\(taskId)")
store.openPage(.preview(taskId: taskId, port: Int(q["port"] ?? "") ?? 0))
case "tab":
overlays.leftTab = LeftTab(rawValue: q["name"] ?? "") ?? .lead
overlays.tabFocus += 1
case "switcher":
overlays.openSwitcher(store)
case "todo":
store.startTodo(q["project"] ?? "", q["title"] ?? "", discuss: q["discuss"] == "1", overlays)
case "suggest":
@@ -676,6 +681,10 @@ private final class FlowTextView: NSTextView {
NSCursor.arrow.set()
return
}
if clickable(event) {
NSCursor.arrow.set()
return
}
super.cursorUpdate(with: event)
}
@@ -684,6 +693,13 @@ private final class FlowTextView: NSTextView {
return
}
super.mouseMoved(with: event)
if clickable(event) {
NSCursor.arrow.set()
}
}
private func clickable(_ event: NSEvent) -> Bool {
onTap != nil && !link(at: convert(event.locationInWindow, from: nil))
}
private func frontmost(_ event: NSEvent) -> Bool {
+24 -2
View File
@@ -192,7 +192,9 @@ private struct TaskPane: View {
VStack(spacing: 0) {
TaskBar()
VStack(spacing: 0) {
if store.selectedTaskId == nil {
if overlays.taskList {
TaskList()
} else if store.selectedTaskId == nil {
ProjectView()
} else {
TabStrip()
@@ -208,6 +210,12 @@ private struct TaskPane: View {
}
.environment(\.paneAccent, accent)
.environment(\.inTask, true)
.onChange(of: store.reveals) {
overlays.taskList = false
}
.onChange(of: store.focus) {
overlays.taskList = false
}
.onGeometryChange(for: CGRect.self) { g in
g.frame(in: .global)
} action: { frame in
@@ -260,7 +268,7 @@ struct TaskBar: View {
var body: some View {
let task = store.selectedTaskId.flatMap { store.findTask($0) }
let project = store.focusedProject()
let label = task.map { "\(project?.name ?? "") ▸ \($0.name)" } ?? project?.name ?? "No task"
let label = overlays.taskList ? "All tasks" : task.map { "\(project?.name ?? "") ▸ \($0.name)" } ?? project?.name ?? "No task"
HStack(spacing: 0) {
Color.clear.frame(maxWidth: .infinity, maxHeight: 1)
Button {
@@ -290,6 +298,20 @@ struct TaskBar: View {
PreviewLink(preview: p)
}
}
#if os(macOS)
Button {
overlays.taskList.toggle()
} label: {
Image(systemName: "list.bullet")
.font(Fonts.font(13, weight: 600))
.foregroundStyle(accent)
.padding(.vertical, 6)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.help(overlays.taskList ? "Back to the task" : "Task list")
.accessibilityLabel("Task list")
#endif
}
.frame(maxWidth: .infinity)
}
+23 -64
View File
@@ -56,6 +56,7 @@ struct PhoneView: View {
@Environment(Overlays.self) private var overlays
@ViewState private var tab = PhoneTab.lead
@ViewState private var path: [PhoneRoute] = []
@ViewState private var moreRoute = MoreRoute.status
var body: some View {
VStack(spacing: 0) {
@@ -73,7 +74,7 @@ struct PhoneView: View {
}
}
shown(.more) {
MoreScreen()
MoreScreen(route: $moreRoute)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
@@ -104,6 +105,24 @@ struct PhoneView: View {
.onChange(of: overlays.leadFocus) {
tab = .lead
}
.onChange(of: overlays.tabFocus) {
switch overlays.leftTab {
case .lead, .server:
tab = .lead
case .status:
tab = .more
moreRoute = .status
case .notes:
tab = .more
moreRoute = .notes
case .workflows:
tab = .more
moreRoute = .workflows
case .settings:
tab = .more
moreRoute = .settings
}
}
.onChange(of: tab == .tasks && !path.isEmpty, initial: true) { _, shown in
store.paneFocus.setShown(shown)
}
@@ -258,69 +277,9 @@ private struct ConnectionMark: View {
}
private struct TaskListScreen: View {
@Environment(Store.self) private var store
@Environment(Overlays.self) private var overlays
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
ForEach(store.projects, id: \.id) { project in
let active = (store.tasks[project.id] ?? []).filter { $0.status == .active }
Button {
store.sheet = nil
store.focusProject(project.id)
} label: {
HStack(alignment: .firstTextBaseline, spacing: 6) {
Text(project.name)
.font(Fonts.font(13, weight: 700))
Text(project.defaultBranch)
.font(Fonts.font(11))
.foregroundStyle(Palette.muted)
Spacer(minLength: 0)
Image(systemName: "chevron.right")
.font(Fonts.font(11))
.foregroundStyle(Palette.muted)
}
.padding(.horizontal, 10)
.padding(.top, 16)
.padding(.bottom, 6)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
if active.isEmpty {
Text("No active tasks.")
.foregroundStyle(Palette.muted)
.padding(.horizontal, 10)
.padding(.vertical, 6)
}
TaskRows(active: active)
}
}
.padding(.horizontal, 8)
.padding(.bottom, 16)
}
.safeAreaInset(edge: .top, spacing: 0) {
HStack {
Spacer(minLength: 0)
Button("New task") {
overlays.openSwitcher(store)
}
.buttonStyle(WebButtonStyle())
.onGeometryChange(for: CGRect.self) { g in
g.frame(in: .named("window"))
} action: { frame in
overlays.pickerFrame = frame
}
}
.padding(.horizontal, 16)
.padding(.vertical, 6)
.background(Palette.bg)
.overlay(alignment: .bottom) {
Rectangle().fill(Palette.border).frame(height: 1)
}
}
.background(Palette.bg)
.toolbar(.hidden, for: .navigationBar)
TaskList()
.toolbar(.hidden, for: .navigationBar)
}
}
@@ -368,7 +327,7 @@ private func dismissKeyboard() {
}
private struct MoreScreen: View {
@ViewState private var route = MoreRoute.status
@Binding var route: MoreRoute
var body: some View {
VStack(spacing: 0) {
+66
View File
@@ -215,6 +215,72 @@ private func rowIndent(_ task: ByreTask, _ depth: Int) -> CGFloat {
task.parentTaskId != nil ? 20 * CGFloat(depth - 1) : 0
}
struct TaskList: View {
@Environment(Store.self) private var store
@Environment(Overlays.self) private var overlays
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
ForEach(store.projects, id: \.id) { project in
let active = (store.tasks[project.id] ?? []).filter { $0.status == .active }
Button {
store.sheet = nil
store.focusProject(project.id)
} label: {
HStack(alignment: .firstTextBaseline, spacing: 6) {
Text(project.name)
.font(Fonts.font(13, weight: 700))
Text(project.defaultBranch)
.font(Fonts.font(11))
.foregroundStyle(Palette.muted)
Spacer(minLength: 0)
Image(systemName: "chevron.right")
.font(Fonts.font(11))
.foregroundStyle(Palette.muted)
}
.padding(.horizontal, 10)
.padding(.top, 16)
.padding(.bottom, 6)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
if active.isEmpty {
Text("No active tasks.")
.foregroundStyle(Palette.muted)
.padding(.horizontal, 10)
.padding(.vertical, 6)
}
TaskRows(active: active)
}
}
.padding(.horizontal, 8)
.padding(.bottom, 16)
}
.safeAreaInset(edge: .top, spacing: 0) {
HStack {
Spacer(minLength: 0)
Button("New task") {
overlays.openSwitcher(store)
}
.buttonStyle(WebButtonStyle())
.onGeometryChange(for: CGRect.self) { g in
g.frame(in: .named("window"))
} action: { frame in
overlays.pickerFrame = frame
}
}
.padding(.horizontal, 16)
.padding(.vertical, 6)
.background(Palette.bg)
.overlay(alignment: .bottom) {
Rectangle().fill(Palette.border).frame(height: 1)
}
}
.background(Palette.bg)
}
}
struct TaskRows: View {
let active: [ByreTask]
@Environment(Store.self) private var store
+196
View File
@@ -0,0 +1,196 @@
import Foundation
import Network
#if os(iOS)
import UIKit
#endif
private let succeeded: [UInt8] = [5, 0, 0, 1, 0, 0, 0, 0, 0, 0]
private let hostUnreachable: UInt8 = 4
private let commandNotSupported: UInt8 = 7
private let addressNotSupported: UInt8 = 8
final class TailnetProxy: @unchecked Sendable {
let credential = (0..<32).map { _ in String(UInt8.random(in: 0..<16), radix: 16) }.joined()
private(set) var port: UInt16 = 0
private let queue = DispatchQueue(label: "run.fc.byre.proxy")
private var listener: NWListener?
private var splices: [ObjectIdentifier: Splice] = [:]
init() {
let bound = DispatchSemaphore(value: 0)
queue.async {
self.listen {
bound.signal()
}
}
bound.wait()
#if os(iOS)
NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: nil) { [queue] _ in
queue.async {
self.listen()
}
}
#endif
}
// iOS reclaims a suspended app's listening sockets, so the listener is bound again on the same port each time the app returns to the foreground.
private func listen(_ bound: (@Sendable () -> Void)? = nil) {
self.listener?.cancel()
self.listener = nil
let params = NWParameters.tcp
params.allowLocalEndpointReuse = true
params.requiredLocalEndpoint = .hostPort(host: .ipv4(.loopback), port: port == 0 ? .any : NWEndpoint.Port(rawValue: port)!)
let listener: NWListener
do {
listener = try NWListener(using: params)
} catch {
failed(nil, error)
return
}
self.listener = listener
listener.stateUpdateHandler = { [weak self, weak listener] state in
guard let self, let listener, self.listener === listener else {
return
}
switch state {
case .ready:
if self.port == 0 {
self.port = listener.port!.rawValue
}
bound?()
case .failed(let error):
self.failed(listener, error)
default:
break
}
}
listener.newConnectionHandler = { [weak self] conn in
self?.accept(conn)
}
listener.start(queue: queue)
}
private func failed(_ listener: NWListener?, _ error: any Error) {
if port == 0 {
fatalError("tailnet proxy listener: \(error)")
}
log.error("tailnet proxy listener on port \(self.port): \(String(describing: error), privacy: .public)")
listener?.cancel()
queue.asyncAfter(deadline: .now() + 1) {
if self.listener === listener {
self.listen()
}
}
}
private func accept(_ conn: NWConnection) {
conn.start(queue: queue)
read(conn, 2) { head in
self.read(conn, Int(head[1])) { methods in
guard head[0] == 5, methods.contains(2) else {
self.send(conn, [5, 0xff], close: true)
return
}
self.send(conn, [5, 2])
self.authenticate(conn)
}
}
}
private func authenticate(_ conn: NWConnection) {
read(conn, 2) { head in
self.read(conn, Int(head[1])) { _ in
self.read(conn, 1) { length in
self.read(conn, Int(length[0])) { password in
guard head[0] == 1, String(decoding: password, as: UTF8.self) == self.credential else {
self.send(conn, [1, 1], close: true)
return
}
self.send(conn, [1, 0])
self.request(conn)
}
}
}
}
}
private func request(_ conn: NWConnection) {
read(conn, 4) { head in
guard head[0] == 5, head[1] == 1 else {
self.refuse(conn, commandNotSupported)
return
}
let connect = { @Sendable (host: String) in
self.read(conn, 2) { port in
self.connect(conn, "\(host):\(Int(port[0]) << 8 | Int(port[1]))")
}
}
switch head[3] {
case 1:
self.read(conn, 4) { ip in
connect(ip.map(String.init).joined(separator: "."))
}
case 3:
self.read(conn, 1) { length in
self.read(conn, Int(length[0])) { name in
connect(String(decoding: name, as: UTF8.self))
}
}
case 4:
self.read(conn, 16) { ip in
connect("[\(IPv6Address(Data(ip))!)]")
}
default:
self.refuse(conn, addressNotSupported)
}
}
}
private func connect(_ conn: NWConnection, _ address: String) {
DispatchQueue.global().async {
do {
let fd = try tailnetNode.dial(address)
self.queue.async {
self.send(conn, succeeded)
let splice = Splice(conn, fd, self.queue)
let id = ObjectIdentifier(splice)
self.splices[id] = splice
splice.start {
self.splices[id] = nil
}
}
} catch {
log.error("tailnet proxy: \(errorText(error), privacy: .public)")
self.queue.async {
self.refuse(conn, hostUnreachable)
}
}
}
}
private func refuse(_ conn: NWConnection, _ code: UInt8) {
send(conn, [5, code, 0, 1, 0, 0, 0, 0, 0, 0], close: true)
}
private func read(_ conn: NWConnection, _ count: Int, _ then: @escaping @Sendable ([UInt8]) -> Void) {
if count == 0 {
then([])
return
}
conn.receive(minimumIncompleteLength: count, maximumLength: count) { data, _, _, error in
guard error == nil, let data, data.count == count else {
conn.cancel()
return
}
then([UInt8](data))
}
}
private func send(_ conn: NWConnection, _ bytes: [UInt8], close: Bool = false) {
conn.send(content: Data(bytes), completion: .contentProcessed { _ in
if close {
conn.cancel()
}
})
}
}
+2 -2
View File
@@ -409,11 +409,11 @@ struct ServerView: View {
Text("Agents' permissions")
.font(Fonts.font(13, weight: 700))
.padding(.top, 16)
Text("The macOS privacy grants the server's agents use to see and control this Mac. Checking asks macOS for any that are missing.")
Text("The macOS privacy grants the server's agents use to see and control this Mac and read its protected folders. Checking asks macOS for any that are missing.")
.foregroundStyle(Palette.muted)
.fixedSize(horizontal: false, vertical: true)
PermissionsCheck {
try await API(origin: origin).client.checkPermissions().ok.body.json
try await API(origin: origin, timeout: 150).client.checkPermissions().ok.body.json
}
Text("Usage commands")
.font(Fonts.font(13, weight: 700))
+42 -1
View File
@@ -22,6 +22,13 @@ struct SettingsView: View {
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
#if os(iOS)
heading("This iPhone's name")
Text("byre shows this iPhone by this name, and names its tailnet node after it. A new name takes effect the next time byre opens.")
.foregroundStyle(Palette.muted)
.fixedSize(horizontal: false, vertical: true)
DeviceNameField()
#endif
heading("Window color")
Text("Windows for this server have a border in this color, on every device you use.")
.foregroundStyle(Palette.muted)
@@ -56,7 +63,7 @@ struct SettingsView: View {
SyncState()
#if os(macOS)
heading("This Mac's permissions")
Text("The macOS privacy grants byre's app needs for agents to see and control this Mac. Checking asks macOS for any that are missing.")
Text("The macOS privacy grants byre's app needs for agents to see and control this Mac and read its protected folders. Checking asks macOS for any that are missing.")
.foregroundStyle(Palette.muted)
.fixedSize(horizontal: false, vertical: true)
PermissionsCheck(run: checkLocalPermissions)
@@ -79,6 +86,40 @@ struct SettingsView: View {
}
}
#if os(iOS)
private struct DeviceNameField: View {
@ViewState private var name = UserDefaults.standard.string(forKey: deviceNameKey) ?? deviceKind
@ViewState private var saved = UserDefaults.standard.string(forKey: deviceNameKey) ?? deviceKind
var body: some View {
let trimmed = name.trimmingCharacters(in: .whitespaces)
let valid = !slug(trimmed).isEmpty && trimmed != saved
HStack(spacing: 6) {
TextField("Name", text: $name)
.modifier(FieldStyle())
.onSubmit {
save(trimmed, valid)
}
Button("Save") {
save(trimmed, valid)
}
.buttonStyle(WebButtonStyle())
.disabled(!valid)
}
.padding(.top, 8)
}
private func save(_ trimmed: String, _ valid: Bool) {
if !valid {
return
}
UserDefaults.standard.set(trimmed, forKey: deviceNameKey)
saved = trimmed
name = trimmed
}
}
#endif
private struct ServerColorPicker: View {
@Environment(Store.self) private var store
+83 -37
View File
@@ -97,7 +97,8 @@ final class Store {
@ObservationIgnored private var historyLoads: Set<ObjectIdentifier> = []
@ObservationIgnored private var requestSeqs: [String: Int] = [:]
@ObservationIgnored private var taskReloads: Set<String> = []
@ObservationIgnored private var taskReloadAgain: Set<String> = []
@ObservationIgnored private var listReloads: Set<String> = []
@ObservationIgnored private var taskRefetches: [String: Set<String>] = [:]
@ObservationIgnored private var terminalChanges = 0
@ObservationIgnored private var streamOpens = 0
@ObservationIgnored private var globalStream: EventStream?
@@ -368,12 +369,70 @@ final class Store {
}
func loadTasks(_ projectId: String) async throws {
let latest = claimRequest("tasks:\(projectId)")
let list = try await api.client.listTasks(path: .init(projectId: projectId)).ok.body.json
if !latest() {
try await reloadTasks(projectId, list: true)
}
// Loads run one at a time per project, so a slower, older response never lands over a newer one.
private func reloadTasks(_ projectId: String, list: Bool = false, task: String? = nil) async throws {
if list {
listReloads.insert(projectId)
}
if let task {
taskRefetches[projectId, default: []].insert(task)
}
guard taskReloads.insert(projectId).inserted else {
return
}
tasks[projectId] = list
defer {
taskReloads.remove(projectId)
}
var failure: (any Error)?
while listReloads.contains(projectId) || taskRefetches[projectId] != nil {
do {
if listReloads.remove(projectId) != nil {
try await loadActiveTasks(projectId)
} else {
try await refetchTasks(projectId, taskRefetches.removeValue(forKey: projectId)!)
}
} catch {
failure = error
}
}
if let failure {
throw failure
}
}
private func loadActiveTasks(_ projectId: String) async throws {
let list = try await api.client.listActiveTasks(path: .init(projectId: projectId)).ok.body.json
let fresh = Set(list.map(\.id))
let held = (tasks[projectId] ?? []).filter { !fresh.contains($0.id) }
for t in held where t.status == .active {
taskRefetches[projectId, default: []].insert(t.id)
}
try await applyTasks(projectId, list + held)
}
private func refetchTasks(_ projectId: String, _ ids: Set<String>) async throws {
let api = api
let fetched = try await withThrowingTaskGroup(of: ByreTask.self) { group in
for id in ids {
group.addTask {
try await api.client.getTask(path: .init(taskId: id)).ok.body.json
}
}
var out: [ByreTask] = []
for try await t in group {
out.append(t)
}
return out
}
let fresh = Set(fetched.map(\.id))
try await applyTasks(projectId, fetched + (tasks[projectId] ?? []).filter { !fresh.contains($0.id) })
}
private func applyTasks(_ projectId: String, _ list: [ByreTask]) async throws {
tasks[projectId] = list.sorted { $0.createdAt > $1.createdAt }
for t in list where t.status == .active && !t.background {
_ = taskFeed(t.id)
}
@@ -384,26 +443,15 @@ final class Store {
}
}
private func reloadTasks(_ projectId: String) async throws {
guard taskReloads.insert(projectId).inserted else {
taskReloadAgain.insert(projectId)
private func lookUpTask(_ taskId: String) {
if findTask(taskId) != nil {
return
}
defer {
taskReloads.remove(projectId)
taskReloadAgain.remove(projectId)
}
var failure: (any Error)?
repeat {
taskReloadAgain.remove(projectId)
do {
try await loadTasks(projectId)
} catch {
failure = error
spawn("load task") {
let t = try await self.api.client.getTask(path: .init(taskId: taskId)).ok.body.json
if self.findTask(taskId) == nil {
self.tasks[t.projectId] = ((self.tasks[t.projectId] ?? []) + [t]).sorted { $0.createdAt > $1.createdAt }
}
} while taskReloadAgain.contains(projectId)
if let failure {
throw failure
}
}
@@ -674,6 +722,7 @@ final class Store {
markVisited(taskId)
selectedTaskId = taskId
if let taskId {
lookUpTask(taskId)
_ = taskFeed(taskId)
}
pinned = nil
@@ -798,11 +847,7 @@ final class Store {
forwards.close(taskId)
}
notifyTask(self, ev)
if let projectId = ev.projectId {
spawn("load tasks") {
try await self.reloadTasks(projectId)
}
}
reloadTask(ev)
case "session.created", "session.state", "session.subagents", "session.model":
if ev.owner != nil {
spawn("load lead sessions") {
@@ -821,11 +866,7 @@ final class Store {
spawn("load inbox") {
try await self.loadInbox()
}
if let projectId = ev.projectId {
spawn("load tasks") {
try await self.reloadTasks(projectId)
}
}
reloadTask(ev)
case "inbox.updated":
clearInbox(try! decodePayload(ev.payload, as: InboxItem.self))
spawn("load inbox") {
@@ -839,11 +880,7 @@ final class Store {
}
case "session.error", "session.rate_limited":
notifyError(self, ev)
if let projectId = ev.projectId {
spawn("load tasks") {
try await self.reloadTasks(projectId)
}
}
reloadTask(ev)
case "task.merge_facts":
if let projectId = ev.projectId, let taskId = ev.taskId, let i = tasks[projectId]?.firstIndex(where: { $0.id == taskId }) {
tasks[projectId]![i].mergeFacts = try! decodePayload(ev.payload, as: MergeFacts.self)
@@ -876,6 +913,15 @@ final class Store {
events.send(ev)
}
private func reloadTask(_ ev: ByreEvent) {
guard let projectId = ev.projectId, let taskId = ev.taskId else {
return
}
spawn("load task") {
try await self.reloadTasks(projectId, task: taskId)
}
}
private func updateTask(_ id: String, _ change: (inout ByreTask) -> Void) {
for (projectId, list) in tasks {
if let i = list.firstIndex(where: { $0.id == id }) {
+2 -5
View File
@@ -18,10 +18,7 @@ func runGo(_ key: String, _ store: Store, _ overlays: Overlays) {
case "p":
overlays.switcher?.close()
store.sheet = nil
if let project = store.focusedProject() ?? store.projects.first {
overlays.projectFocus = true
store.focusProject(project.id)
}
overlays.taskList = true
case "l":
overlays.switcher?.close()
store.sheet = nil
@@ -73,7 +70,7 @@ private let shortcuts: [(String, [(String, String)])] = [
("Anywhere", [
("⌘K", "Switch task, or create one"),
("⌘N", "New task in the current project"),
("⌘P", "Show the current project's dashboard"),
("⌘P", "Show every project's active tasks"),
("⌘L", "Open the Lead tab and its composer, closing any view opened over a pane"),
("⌘S", "Merge the right pane's task, or discard it when the agent suggests discarding"),
("⌘[ ⌘]", "Bring the previous or next server's window forward"),
+3 -16
View File
@@ -27,9 +27,7 @@ func slug(_ text: String) -> String {
final class TailnetNode: Sendable {
let sd: tailscale
let hostname = "byre-app-\(slug(deviceName))"
private let proxyHost: String
private let proxyPort: UInt16
private let proxyCredential: String
private let socks = TailnetProxy()
init() {
let dir = URL.applicationSupportDirectory.appending(path: "run.fc.byre.mac/tsnet", directoryHint: .isDirectory)
@@ -43,17 +41,6 @@ final class TailnetNode: Sendable {
Self.check(sd, "set dir", unsafe tailscale_set_dir(sd, dir.path))
Self.check(sd, "set ephemeral", tailscale_set_ephemeral(sd, 0))
Self.check(sd, "start", tailscale_start(sd))
var addr = [CChar](repeating: 0, count: 64)
var proxyCred = [CChar](repeating: 0, count: 33)
var apiCred = [CChar](repeating: 0, count: 33)
Self.check(sd, "loopback", unsafe tailscale_loopback(sd, &addr, addr.count, &proxyCred, &apiCred))
let address = String(decoding: addr.prefix { $0 != 0 }.map { UInt8(bitPattern: $0) }, as: UTF8.self)
guard let colon = address.lastIndex(of: ":"), let port = UInt16(address[address.index(after: colon)...]) else {
fatalError("tsnet loopback address \(address)")
}
proxyHost = String(address[..<colon])
proxyPort = port
proxyCredential = String(decoding: proxyCred.prefix { $0 != 0 }.map { UInt8(bitPattern: $0) }, as: UTF8.self)
}
static func check(_ sd: tailscale, _ what: String, _ result: Int32) {
@@ -69,8 +56,8 @@ final class TailnetNode: Sendable {
}
func proxy() -> ProxyConfiguration {
let proxy = ProxyConfiguration(socksv5Proxy: .hostPort(host: NWEndpoint.Host(proxyHost), port: NWEndpoint.Port(rawValue: proxyPort)!))
proxy.applyCredential(username: "tsnet", password: proxyCredential)
let proxy = ProxyConfiguration(socksv5Proxy: .hostPort(host: .ipv4(.loopback), port: NWEndpoint.Port(rawValue: socks.port)!))
proxy.applyCredential(username: "byre", password: socks.credential)
return proxy
}
+13 -2
View File
@@ -536,11 +536,16 @@ final class TranscriptBuilder {
continue
}
if ev.kind == "task.forked", let fork = str(p, "fork") {
if let row = ctx.snapshots[snapKey(int(p, "forked_at") ?? 0)] {
let at = int(p, "forked_at") ?? 0
if at == 0 {
put(Row(.note(Note(parts: [.text("\(ev.actor) continued this task as "), .task(fork)], when: ev.createdAt)), taskId: rowTask))
continue
}
if let row = ctx.snapshots[snapKey(at)] {
row.forks.append(fork)
continue
}
put(Row(.note(Note(parts: [.text("\(ev.actor) forked snapshot #\(jsString(p["forked_at"])) as "), .task(fork)], when: ev.createdAt)), taskId: rowTask))
put(Row(.note(Note(parts: [.text("\(ev.actor) forked snapshot #\(at) as "), .task(fork)], when: ev.createdAt)), taskId: rowTask))
continue
}
if ev.kind == "review.started" || ev.kind == "review.finished" || ev.kind == "review.finding" {
@@ -913,6 +918,12 @@ final class TranscriptBuilder {
return note([.text("\(jsString(p["error"])) (\(jsString(p["vendor"])) \(limit["rateLimitType"].map { jsString($0) } ?? "") limit resets \(resets))")], .error)
case "task.created":
if let from = str(p, "forked_from"), p["forked_at"] == nil {
if let task = rowTask, task != taskId {
return note([.text("\(ev.actor) continued "), .task(from), .text(" as "), .task(task), .text(" here")])
}
return note([.text("\(ev.actor) continued "), .task(from), .text(" here in a new task, started from main; everything above happened in that task, and its agent continues that conversation")])
}
if let from = str(p, "forked_from"), let task = rowTask, task != taskId {
return note([.text("\(ev.actor) forked "), .task(task), .text(" here from "), .task(from), .text(" at snapshot #\(jsString(p["forked_at"]))")])
}
+4 -2
View File
@@ -7,5 +7,7 @@ mac=$(cd "$(dirname "$0")" && pwd)
rm -rf "$mac/.build/phone/iphoneos/export"
xcodebuild -exportArchive -archivePath "$mac/.build/phone/iphoneos/Byre.xcarchive" -exportOptionsPlist "$mac/phone/export.plist" -exportPath "$mac/.build/phone/iphoneos/export" -allowProvisioningUpdates
version=$(plutil -extract CFBundleShortVersionString raw "$mac/.build/Byre.app/Contents/Info.plist")
dmg=$mac/.build/release/$version/byre-$version.dmg
"$mac/install.sh" "$dmg"
mkdir -p "$HOME/byre-deploy"
cp "$mac/.build/release/$version/byre-$version.dmg" "$HOME/byre-deploy/byre.dmg"
cp "$mac/install.sh" "$HOME/byre-deploy/install.sh"
echo "$HOME/byre-deploy/byre.dmg"
+8
View File
@@ -10,3 +10,11 @@ rm -rf /Applications/Byre.app
mv /Applications/Byre.app.new /Applications/Byre.app
plutil -extract CFBundleVersion raw /Applications/Byre.app/Contents/Info.plist
plutil -extract ByreCommit raw /Applications/Byre.app/Contents/Info.plist
app='^/Applications/Byre.app/Contents/MacOS/Byre$'
if pgrep -q -a -f "$app"; then
osascript -e 'tell application id "run.fc.byre.mac" to quit'
while pgrep -q -a -f "$app"; do
sleep 0.2
done
open /Applications/Byre.app
fi