15 Commits
Author SHA1 Message Date
Ian Gulliver e1eeaad925 Use one build number across phone, Mac and Status 2026-10-22 07:19:28 -07:00
Ian Gulliver 9952170aae Remove unused raw protocol recorders from agent sessions 2026-10-22 07:14:44 -07:00
Ian Gulliver 19bb6767ff Mark phones in the Status chart and fit it to phone width 2026-10-22 07:12:12 -07:00
Ian Gulliver 7997be6373 Make Status the first tab under More on the phone and after Lead on Mac 2026-10-22 07:09:31 -07:00
Ian Gulliver cbbfe43b94 Make starting a new task on iPhone one tap 2026-10-22 07:05:52 -07:00
Ian Gulliver 0a53017319 Show Tailscale sign-in in an in-app web view 2026-10-22 07:03:56 -07:00
Ian Gulliver eac5db5dad Always notify the lead when a task completes 2026-10-22 07:00:10 -07:00
Ian Gulliver 9424ba9cc3 Let discussion-only tasks ignore the merge/discard nudge 2026-10-22 06:59:41 -07:00
Ian Gulliver 188a458930 Fix phone keyboard and touch targets, render notes as Markdown 2026-10-22 06:33:03 -07:00
Ian Gulliver e2d747d7d3 Tell agents to redo merge, discard or ask after a nudge 2026-10-21 22:54:02 -07:00
Ian Gulliver 660058f0c0 Dismiss the iPhone keyboard on pages with no text field 2026-10-21 22:43:18 -07:00
Ian Gulliver 5b1015d006 Allow pasting and attaching images in question Other fields on iPhone 2026-10-21 22:41:00 -07:00
Ian Gulliver 5384c83a0f Focus the project dashboard when ⌘P opens it 2026-10-21 22:38:16 -07:00
Ian Gulliver 4f2c0def94 Add a task list button to the iPhone bottom bar 2026-10-21 22:34:55 -07:00
Ian Gulliver caf0a830a9 Let agents show and drive Page view tabs on the iPhone 2026-10-21 22:31:48 -07:00
39 changed files with 714 additions and 653 deletions
+3 -3
View File
@@ -13,8 +13,8 @@ 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 what you deployed and what you checked.
3. Once the service is confirmed healthy, update each client Mac whose app is running, through the client tools and without asking first: upload the disk image (`mac/.build/release/<version>/byre-<version>.dmg`) and `mac/install.sh`, quit the app, run `install.sh <dmg>`, and `open /Applications/Byre.app`. The app reads its server address from its iCloud-synced SwiftData store, so it reconnects to prod with no setup. Report each Mac's app commit (`install.sh` prints it).
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.
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).
@@ -52,7 +52,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), Notes (what the lead has noted about the user), Workflows (the saved workflows), Status, 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; 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
+4 -2
View File
@@ -2312,12 +2312,14 @@ components:
ConnectedClient:
type: object
required: [id, login, host, commit, connected_at]
required: [id, login, host, device, commit, build, connected_at]
properties:
id: { type: string, description: The ID the app made up when it started, from its Byre-Client header. }
login: { type: string }
host: { type: string, description: The Mac's name from its Byre-Host header. }
host: { type: string, description: The device's name from its Byre-Host header. }
device: { type: string, description: "The kind of device the app runs on (Mac or iPhone), from its Byre-Device header." }
commit: { type: string, description: The git commit the app was built from, from its Byre-Commit header; empty when the client sent none. }
build: { type: string, description: "The app's build number (its CFBundleVersion, the Unix time of that commit), from its Byre-Build header; empty when the client sent none." }
connected_at: { type: string, format: date-time, description: When the client's WebSocket connected. }
last_action_at: { type: string, format: date-time, description: When byre last received a request the human made from this client since byre started; absent when none. }
sync: { $ref: "#/components/schemas/ClientSync" }
+7 -3
View File
@@ -1,15 +1,19 @@
# byre for iPhone
The iPhone app is the same `Byre` target as the Mac app, built for iOS: platform differences are `#if os(macOS)` / `#if os(iOS)` blocks, with the small shared aliases (`PlatformImage`, `PlatformColor`, `PlatformFont`, `PlatformView`, `touchOnly`, `deviceKind`) in `mac/Sources/Byre/Platform.swift`. Only the layout is the phone's own (`Phone.swift`): a tab bar with Lead, Tasks and More. Tasks lists every project's active tasks with the Mac's own task rows; a task opens pushed, with the Mac's task bar and tab strip on top and the tab's view below in place of the Mac's attached sheet. Zoomed images and diagrams cover the screen. More holds Notes, Workflows, Status and Settings. Every action that focuses a task or project (a row, a link, a notification, the switcher) bumps `Store.reveals`, which brings the Tasks tab forward with the task open; the server's own focus changes don't.
The iPhone app is the same `Byre` target as the Mac app, built for iOS: platform differences are `#if os(macOS)` / `#if os(iOS)` blocks, with the small shared aliases (`PlatformImage`, `PlatformColor`, `PlatformFont`, `PlatformView`, `touchOnly`, `deviceKind`) in `mac/Sources/Byre/Platform.swift`. Only the layout is the phone's own (`Phone.swift`): a tab bar with Lead, Tasks and More. Tasks lists every project's active tasks with the Mac's own task rows; a task opens pushed, with the Mac's task bar and tab strip on top and the tab's view below in place of the Mac's attached sheet. Zoomed images and diagrams cover the screen. More holds Status (shown first), Notes, Workflows and Settings. Every action that focuses a task or project (a row, a link, a notification, the switcher) bumps `Store.reveals`, which brings the Tasks tab forward with the task open; the server's own focus changes don't.
Left out on the phone: the Server tab and running byre locally, the client connection (`Remote`: the phone is never a client for agents' client tools, so `list_clients` doesn't show it), the Sparkle updater, and keyboard shortcuts. Notifications come from the app's own event stream, so they only show while the app is open; push notifications would need APNs on the server.
The phone holds the client connection (`Remote`) like a Mac, so agents can show it pages and files and drive its Page view (`show_page`, `show_file`, `drive_page`); it answers every other client request with an error. It sends `Byre-Device: iPhone` (Macs send `Mac`), and the server signs commits and runs `byre client-exec` without `--client` only on Macs.
Left out on the phone: the Server tab and running byre locally, the shell, file, hands and signing client requests, the Sparkle updater, and keyboard shortcuts. Notifications come from the app's own event stream, so they only show while the app is open; push notifications would need APNs on the server.
## Building
`mac/phone.sh` builds the app with `xcodebuild` (a Swift package can't be built for iOS by `swift build`) in the same shared directory and under the same lock as `mac/bundle.sh`, assembles `Byre.app` itself (icon through `actool`, fonts, `Info.plist` from `mac/phone/Info.plist` plus the version, commit and `DT*` keys App Store Connect checks), signs it ad hoc with `mac/phone/byre.entitlements`, and wraps it in `mac/.build/phone/iphoneos/Byre.xcarchive`. Release only, like the Mac app. `xcodebuild -exportArchive -archivePath <archive> -exportOptionsPlist mac/phone/export.plist -exportPath <dir> -allowProvisioningUpdates` re-signs it for the App Store and uploads it to TestFlight, using the Apple account Xcode is signed into: Xcode makes the distribution certificate and profile itself, so none are kept in the repo. Uploading needs the App ID `run.fc.byre.ios` (iCloud with the container `iCloud.run.fc.byre.mac`, and Push Notifications) and its app record in App Store Connect; neither can be made through Apple's API. The version is the last release tag's (App Store Connect takes only numbers) and the build number the UTC build time, `YYYYMMDD.HHMMSS`: App Store Connect refuses a build number that isn't higher than every earlier upload, and a commit count can go down when a branch with many commits uploads before its squash merge lands on main.
`mac/phone.sh` builds the app with `xcodebuild` (a Swift package can't be built for iOS by `swift build`) in the same shared directory and under the same lock as `mac/bundle.sh`, assembles `Byre.app` itself (icon through `actool`, fonts, `Info.plist` from `mac/phone/Info.plist` plus the version, commit and `DT*` keys App Store Connect checks), signs it ad hoc with `mac/phone/byre.entitlements`, and wraps it in `mac/.build/phone/iphoneos/Byre.xcarchive`. Release only, like the Mac app. `xcodebuild -exportArchive -archivePath <archive> -exportOptionsPlist mac/phone/export.plist -exportPath <dir> -allowProvisioningUpdates` re-signs it for the App Store and uploads it to TestFlight, using the Apple account Xcode is signed into: Xcode makes the distribution certificate and profile itself, so none are kept in the repo. Uploading needs the App ID `run.fc.byre.ios` (iCloud with the container `iCloud.run.fc.byre.mac`, and Push Notifications) and its app record in App Store Connect; neither can be made through Apple's API. The version is the last release tag's (App Store Connect takes only numbers) and the build number is the Mac app's (`docs/operations.md`, "Releases"). App Store Connect refuses a build number that isn't higher than every earlier upload, so a second upload of the same commit fails; a commit count wouldn't do, since it can go down when a branch with many commits uploads before its squash merge lands on main.
`mac/phone.sh --simulator` builds an ad-hoc-signed app for the iOS simulator instead, at `mac/.build/phone/iphonesimulator/Byre.app`; install it with `xcrun simctl install <device> <app>` and start it with `xcrun simctl launch <device> run.fc.byre.ios`. The simulator ignores entitlements given to `codesign` and refuses to launch an ad-hoc app signed with restricted ones, so the script links `mac/phone/byre.entitlements` into the binary's `__TEXT,__entitlements` section, as Xcode does; without the iCloud entitlement CloudKit raises at launch. Its node signs in to Tailscale like any other: `xcrun simctl launch --console-pty` prints libtailscale's sign-in URL, which changes on every launch.
The simulator's window (Xcode's Device Hub) lists the app's own accessibility elements, so hands' `ax_tree` and `act` `press` drive the app without a mouse. Hands' mouse steps (`click`, `drag`, `scroll`) refuse that window, because Device Hub reports no process id; a gesture such as dragging the keyboard away needs mouse events posted with `CGEvent` instead. `xcrun simctl io booted screenshot --display LCD <file>` captures the app at full resolution.
## 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.
+1 -1
View File
@@ -36,7 +36,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. 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` 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.
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.
+4 -3
View File
@@ -36,7 +36,8 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
Description: "List the Macs with byre's app open: the human's own machines, not the one byre (and your worktree) runs on. " +
"Each has its name, which the other client tools take as client, the id of that running app (pass it as client instead when two apps are open on one Mac), the human using it, and when the human last did something in byre from it. " +
"A message from the human says which Mac it was sent from (\"Sent from <name> (client <id>)\"); that is the Mac the human sits at, so show things there, naming it by that id. " +
"The most recently active one is listed first.",
"The most recently active one is listed first. " +
"The human's iPhone is listed too, when byre's app is open on it; it takes only show_page, show_file and drive_page, and the other client tools fail there.",
InputSchema: json.RawMessage(`{"type": "object", "properties": {}}`),
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
return m.listClients(), nil
@@ -289,13 +290,13 @@ func (m *Manager) listClients() string {
now := time.Now()
b := &strings.Builder{}
for i, r := range rows {
fmt.Fprintf(b, "- %s (%s, id %s): ", r.Host, r.Login, r.ID)
fmt.Fprintf(b, "- %s (%s, %s, id %s): ", r.Host, r.Device, r.Login, r.ID)
if r.LastActionAt == nil {
b.WriteString("no action since byre started")
} else {
fmt.Fprintf(b, "last action %s ago", now.Sub(*r.LastActionAt).Round(time.Second))
}
fmt.Fprintf(b, ", connected %s ago, app built from %s", now.Sub(r.ConnectedAt).Round(time.Second), r.Commit)
fmt.Fprintf(b, ", connected %s ago, app build %s from %s", now.Sub(r.ConnectedAt).Round(time.Second), r.Build, r.Commit)
if r.Sync != nil && !r.Sync.Ok {
fmt.Fprintf(b, ", iCloud sync failing since %s ago: %s", now.Sub(r.Sync.At).Round(time.Second), *r.Sync.Error)
}
+1 -1
View File
@@ -11,7 +11,7 @@ byre client-exec [--client NAME] [--timeout SECS] -- CMD...
`byre` is `/Applications/Byre.app/Contents/Helpers/byre` on every Mac with the app.
- It runs CMD as `client_bash` does: zsh as a login shell in the home directory, in the app's session, as the human. That session is what GUI-only commands need.
- Without `--client` it picks the most recently active client, the one `list_clients` lists first.
- Without `--client` it picks the most recently active Mac, skipping the iPhone, which runs no commands.
- Each argument reaches the client as one word, so shell syntax needs `-- zsh -c '...'`.
- The command's stdout and stderr come back separately and byte for byte, once it ends, written to client-exec's own stdout and stderr.
- Exit status: the command's own; 75 when no client is connected, it didn't acknowledge the request within 2s, or it disconnected; 124 when `--timeout` (default 30s) killed it. A one-line `byre client-exec: ...` on stderr says why.
+4 -32
View File
@@ -15,7 +15,7 @@ import (
)
const leadGuidance = `You are the human's lead agent in byre, shown in the left pane of byre's app. The human talks to you there, and you coordinate every task agent on their behalf. You don't do task work yourself: task agents work in their own worktrees, and you read, direct and report on them.
- Signals: byre sends you, as messages from byre, what happens across tasks: questions that task agents ask, errors and rate limits their sessions hit, turns they finish with their final message, a task's agent saying the task is ready to merge (with its overview of the whole task, which byre asks for once the review of its latest work has finished and its turn has ended) or suggesting the human discard it (with its reasoning), questions the human answered, tasks created, reviews started and finished, a merge held behind another task's or set to start after the review (and that canceled or dropped), starting to merge, merged or discarded, and every change of a task's state, as "<task> state: <before> → <now>." listed first among what happened at the same moment. State changes, reviews starting, and tasks the human created, started merging, merged or discarded never wake you on their own; they arrive only alongside another signal. When something waits on a task closing (the human says "after that merge, deploy", or you merged it for them and have more to do), call wake_on with the task right away: its next merged or discarded signal then wakes you, once. The states are running, idle, finished (a workflow stage's task once its finish_stage has taken), review_pending, reviewing, merging, discarding, merged, discarded and no_agent. A finished turn arrives with its state change: review_pending when byre is about to review the turn's work, reviewing while the review runs; neither is done, since the review can still find problems, so don't treat a task's work as finished until its state goes to idle or finished. Several may arrive in one message, followed by the current task list (as list_tasks gives it) and what your right pane shows, so you rarely need to call list_tasks after a signal. The human doesn't see signals. Messages from the human arrive separately.
- Signals: byre sends you, as messages from byre, what happens across tasks: questions that task agents ask, errors and rate limits their sessions hit, turns they finish with their final message, a task's agent saying the task is ready to merge (with its overview of the whole task, which byre asks for once the review of its latest work has finished and its turn has ended) or suggesting the human discard it (with its reasoning), questions the human answered, tasks created, reviews started and finished, a merge held behind another task's or set to start after the review (and that canceled or dropped), starting to merge, merged or discarded, and every change of a task's state, as "<task> state: <before> → <now>." listed first among what happened at the same moment. State changes, reviews starting, and tasks the human created or started merging never wake you on their own; they arrive only alongside another signal. A task merged or discarded always wakes you, whoever closed it. The states are running, idle, finished (a workflow stage's task once its finish_stage has taken), review_pending, reviewing, merging, discarding, merged, discarded and no_agent. A finished turn arrives with its state change: review_pending when byre is about to review the turn's work, reviewing while the review runs; neither is done, since the review can still find problems, so don't treat a task's work as finished until its state goes to idle or finished. Several may arrive in one message, followed by the current task list (as list_tasks gives it) and what your right pane shows, so you rarely need to call list_tasks after a signal. The human doesn't see signals. Messages from the human arrive separately.
- Don't respond to what the human just did. A signal about their own action (a question they answered, a task they created, merged, started merging or discarded) tells them nothing new: don't report, confirm or comment on it. Act on it only if it changes what you are doing, and then end the turn with one short line at most. When a batch of signals needs nothing from you to the human, make your final message exactly NO_REPLY and nothing else; byre folds that turn away so the human doesn't see it.
- "Your right pane is free" means the human finished what was in the right pane. Show them the next thing that needs them, most urgent first (questions, then errors in the task's transcript, then finished turns, then tasks ready to merge or suggested for discarding), with one line saying what it is. If nothing needs them, show that project with no task, then post a short status: one line per active task (what it's doing or waiting on), then up to three TODO items you'd start next (from list_tasks), each with a one-line reason. Write each item as a link to #/todo/<project_id>/<title, URL-encoded with parentheses as %28 and %29>, for example [Fix login redirect](#/todo/prj3f2a9C1b7dQ/Fix%20login%20redirect): byre draws it with a Start button the human can click, and once the task exists, its state. Start one with start_todo only when the human picks it. If that says nothing new since your last status, make your final message NO_REPLY.
- Whenever you suggest new work that isn't in TODO.md (a task you think is worth doing, an improvement to byre or to your own role), write it as a link to #/suggest/<project_id>/<title>/<brief>, both URL-encoded with parentheses as %28 and %29, for example [Lead tool for admin status](#/suggest/prj3f2a9C1b7dQ/Lead%20tool%20for%20admin%20status/Add%20a%20lead%20tool%20that%20reads%20%2Fapi%2Fadmin%2Fstatus%20...). The brief is the new task agent's whole first message, so make it a complete brief, as for create_task. byre draws the link with two buttons: Start creates the task with that title and brief at once, without you, and Discuss sends you a message asking to talk the suggestion over. Once an active task has that title, it shows the task's state instead. Don't ask whether to start a suggestion; the buttons do that.
@@ -427,10 +427,6 @@ func (m *Manager) signal(ctx context.Context, batch []api.Event) error {
if err != nil {
return err
}
woken, err := m.takeWakes(ctx, batch)
if err != nil {
return err
}
batch, err = m.dropBackground(ctx, batch)
if err != nil || len(batch) == 0 {
return err
@@ -450,8 +446,7 @@ func (m *Manager) signal(ctx context.Context, batch []api.Event) error {
lines := []string{}
loud := false
for i, ev := range batch {
wake := closeSignal(ev) && woken[deref(ev.TaskId)][owner]
if ev.Actor == leadActor(owner) && !wake {
if ev.Actor == leadActor(owner) && !closeSignal(ev) {
continue
}
freed, err := m.freedText(ctx, owner, ev)
@@ -462,10 +457,7 @@ func (m *Manager) signal(ctx context.Context, batch []api.Event) error {
if text == "" {
continue
}
if wake {
text += "\nYou asked to be woken by this with wake_on."
}
if wake || freed != "" || !quietSignal(ev) {
if freed != "" || !quietSignal(ev) {
loud = true
}
if ev.Kind == "task.state" {
@@ -490,26 +482,6 @@ func closeSignal(ev api.Event) bool {
return (ev.Kind == "task.merged" || ev.Kind == "task.discarded") && ev.TaskId != nil
}
func (m *Manager) takeWakes(ctx context.Context, batch []api.Event) (map[string]map[string]bool, error) {
woken := map[string]map[string]bool{}
for _, ev := range batch {
if !closeSignal(ev) {
continue
}
owners, err := m.store.TakeLeadWakes(ctx, *ev.TaskId)
if err != nil {
return nil, err
}
for _, owner := range owners {
if woken[*ev.TaskId] == nil {
woken[*ev.TaskId] = map[string]bool{}
}
woken[*ev.TaskId][owner] = true
}
}
return woken, nil
}
func (m *Manager) dropBackground(ctx context.Context, batch []api.Event) ([]api.Event, error) {
kept := []api.Event{}
for _, ev := range batch {
@@ -531,7 +503,7 @@ func quietSignal(ev api.Event) bool {
switch ev.Kind {
case "task.state", "review.started":
return true
case "task.created", "task.merge_started", "task.merged", "task.discarded":
case "task.created", "task.merge_started":
return humanActor(ev.Actor)
}
return false
+1 -24
View File
@@ -408,7 +408,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
"Merge a task as the human: its agent merges the default branch in, then byre squashes it into one commit, pushes it to origin and closes the task. "+
"While another task in the project is merging, byre holds the merge and starts it when that one is done. "+
"For a subtask, target parent instead asks its parent task's agent to merge it into the parent's branch, and the subtask closes once the parent has. "+
"Refused for a task with active subtasks. Only when the human asks. Its merged signal arrives only alongside another signal unless you call wake_on for the task; failures show as errors in the task's transcript, and you get a signal for each."+stoppedRefusal,
"Refused for a task with active subtasks. Only when the human asks. Its merged signal wakes you; failures show as errors in the task's transcript, and you get a signal for each."+stoppedRefusal,
`{"type": "object", "properties": {
"task_id": {"type": "string"},
"target": {"type": "string", "enum": ["main", "parent"], "description": "Default main."}
@@ -489,29 +489,6 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return fmt.Sprintf("Discarding %s: its agent gets a turn to write the task summary, and byre discards the task when that turn ends.", task.Id), nil
}),
tool("wake_on",
"Make an active task's next merged or discarded signal wake you, once, whoever merged or discarded it. "+
"Without it, a merge or discard by the human only arrives alongside another signal. Use it when something waits on that task closing, "+
"for example \"after that merge, deploy\".",
taskOnly,
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
task, _, err := m.taskArg(ctx, raw)
if err != nil {
return "", err
}
if task.Background {
return "", fmt.Errorf("%w: %s is a background task, whose signals never reach you", ErrInvalid, task.Id)
}
err = m.store.AddLeadWake(ctx, user.Login, task.Id)
if errors.Is(err, store.ErrNotFound) {
return "", fmt.Errorf("%w: %s has already been merged or discarded", ErrInvalid, task.Id)
}
if err != nil {
return "", err
}
return fmt.Sprintf("You'll be woken when %s is merged or discarded.", task.Id), nil
}),
tool("show",
"Point the human's right pane at a task, which shows its transcript (a workflow stage task's run: its graph and each stage's state, attempts and outputs), or at a project with no task, which shows the project's dashboard: its active tasks and its TODO.md items. "+
"The task's other views (changes, files, terminal, board, project) open over the human's left pane only when the human opens them; you can't open them. "+
+3 -1
View File
@@ -14,7 +14,9 @@ import (
)
const readyPromptAsk = "If it isn't ready (work remains that you mean to do, or you're waiting on the human), call neither: say what's left in one line and end your turn. " +
"If you're waiting on the human for a decision or input, ask it with ask_human now, even if an earlier message already put it in prose, so it reaches them; only an open ask_human question already counts as asked. Don't change any files this turn."
"If you're waiting on the human for a decision or input, ask it with ask_human now, even if an earlier message already put it in prose, so it reaches them. " +
"If this task is only a discussion with the human (questions, explanations, advice, no work to land) and the discussion is still going, ignore this message: call none of these and end your turn with one line, so the discussion can continue. " +
"If you already called ready_for_merge, suggest_discard or ask_human before this message, and it still applies, call it again now: only a call made after this message counts. Don't change any files this turn."
const readyPrompt = "Your turn has ended and the review of your latest work has finished. If the task is ready to merge, call ready_for_merge with an overview of the whole task, then end your turn with one line. " +
"If the work shouldn't land at all (the approach failed, the task turned out unnecessary or already done), call suggest_discard with your reasoning instead, then end your turn with one line. " +
+1 -1
View File
@@ -15,7 +15,7 @@ const signTimeout = 2 * time.Minute
func (m *Manager) signer(ctx context.Context, user api.User) func(payload []byte) ([]byte, error) {
return func(payload []byte) ([]byte, error) {
mine := []clients.Client{}
for _, c := range m.clients.Recent() {
for _, c := range m.clients.RecentMacs() {
if c.Login == user.Login {
mine = append(mine, c)
}
+355 -348
View File
@@ -882,13 +882,19 @@ type CompactCreate struct {
// ConnectedClient defines model for ConnectedClient.
type ConnectedClient struct {
// Build The app's build number (its CFBundleVersion, the Unix time of that commit), from its Byre-Build header; empty when the client sent none.
Build string `json:"build"`
// Commit The git commit the app was built from
Commit string `json:"commit"`
// ConnectedAt When the client's WebSocket connected.
ConnectedAt time.Time `json:"connected_at"`
// Host The Mac's name from its Byre-Host header.
// Device The kind of device the app runs on (Mac or iPhone), from its Byre-Device header.
Device string `json:"device"`
// Host The device's name from its Byre-Host header.
Host string `json:"host"`
// Id The ID the app made up when it started
@@ -4790,353 +4796,354 @@ 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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",
"hbafr0zNiMRNmIgCBvgOmMmvRkn94cP/NwA=",
"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",
}
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
+9 -57
View File
@@ -41,7 +41,6 @@ type Options struct {
Hooks map[string][]HookMatcher
MCPServers []*mcp.Server
StdioMCP []mcp.Stdio
Transcript io.Writer
OnBadLine func(line []byte, reason string)
}
@@ -197,9 +196,7 @@ type Session struct {
events chan Event
done chan struct{}
writeMu sync.Mutex
transcript io.Writer
scrub []byte
writeMu sync.Mutex
pendingMu sync.Mutex
pending map[string]chan controlResult
@@ -235,15 +232,14 @@ type envelope struct {
func Start(ctx context.Context, opts Options) (*Session, error) {
s := &Session{
events: make(chan Event, 256),
done: make(chan struct{}),
transcript: opts.Transcript,
pending: map[string]chan controlResult{},
inflight: map[string]context.CancelFunc{},
hooks: map[string]HookFunc{},
servers: map[string]*mcp.Server{},
id: initialID(opts),
onBadLine: opts.OnBadLine,
events: make(chan Event, 256),
done: make(chan struct{}),
pending: map[string]chan controlResult{},
inflight: map[string]context.CancelFunc{},
hooks: map[string]HookFunc{},
servers: map[string]*mcp.Server{},
id: initialID(opts),
onBadLine: opts.OnBadLine,
}
s.lastLine.Store(time.Now().UnixNano())
s.ctx, s.stop = context.WithCancel(context.Background())
@@ -524,30 +520,10 @@ func (s *Session) write(v any) error {
s.writeMu.Lock()
defer s.writeMu.Unlock()
s.record("send", b)
_, err = s.stdin.Write(append(b, '\n'))
return err
}
func (s *Session) record(dir string, line []byte) {
if s.transcript == nil {
return
}
if len(s.scrub) > 0 {
line = bytes.ReplaceAll(line, s.scrub, []byte("[redacted]"))
}
b, err := json.Marshal(map[string]any{"dir": dir, "msg": json.RawMessage(line)})
if err != nil {
slog.Error("record transcript", "err", err)
return
}
s.transcript.Write(append(b, '\n'))
}
func (s *Session) readLoop(stdout io.Reader) {
defer close(s.done)
defer close(s.events)
@@ -587,15 +563,6 @@ func (s *Session) handleLine(line []byte) {
s.badLine(line, fmt.Sprintf("unrecognized type %q", env.Type))
}
s.writeMu.Lock()
if env.Type == "control_response" {
s.learnScrub(env.Response)
}
if env.Type != "stream_event" {
s.record("recv", line)
}
s.writeMu.Unlock()
switch env.Type {
case "control_response":
s.handleControlResponse(env.Response)
@@ -638,21 +605,6 @@ func (s *Session) badLine(line []byte, reason string) {
}
}
func (s *Session) learnScrub(raw json.RawMessage) {
resp := struct {
Response struct {
Account struct {
Email string `json:"email"`
} `json:"account"`
} `json:"response"`
}{}
err := json.Unmarshal(raw, &resp)
if err != nil || resp.Response.Account.Email == "" {
return
}
s.scrub = []byte(resp.Response.Account.Email)
}
func (s *Session) handleControlResponse(raw json.RawMessage) {
resp := struct {
Subtype string `json:"subtype"`
+22 -8
View File
@@ -33,6 +33,8 @@ type Client struct {
Login string
Host string
Commit string
Build string
Device string
ConnectedAt time.Time
LastActionAt *time.Time
Sync *api.ClientSync
@@ -69,7 +71,7 @@ func New(changed func()) *Registry {
return &Registry{clients: map[string]*entry{}, transfers: map[string]Transfer{}, changed: changed}
}
func (r *Registry) entry(id, login, host, commit string, now time.Time) *entry {
func (r *Registry) entry(id, login, host, commit, build string, now time.Time) *entry {
for k, e := range r.clients {
if e.conn == nil && now.Sub(e.seen) > expireAfter {
delete(r.clients, k)
@@ -83,18 +85,19 @@ func (r *Registry) entry(id, login, host, commit string, now time.Time) *entry {
e.Login = login
e.Host = host
e.Commit = commit
e.Build = build
e.seen = now
return e
}
func (r *Registry) Acted(id, login, host, commit string) bool {
func (r *Registry) Acted(id, login, host, commit, build string) bool {
if id == "" {
return false
}
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
e := r.entry(id, login, host, commit, now)
e := r.entry(id, login, host, commit, build, now)
e.LastActionAt = &now
if now.Sub(e.notified) < time.Second {
return false
@@ -103,27 +106,28 @@ func (r *Registry) Acted(id, login, host, commit string) bool {
return true
}
func (r *Registry) Synced(id, login, host, commit string, sync api.ClientSync) {
func (r *Registry) Synced(id, login, host, commit, build string, sync api.ClientSync) {
r.mu.Lock()
defer r.mu.Unlock()
r.entry(id, login, host, commit, time.Now()).Sync = &sync
r.entry(id, login, host, commit, build, time.Now()).Sync = &sync
}
func (r *Registry) Serve(ctx context.Context, ws *websocket.Conn, id, login, host, commit string) {
func (r *Registry) Serve(ctx context.Context, ws *websocket.Conn, id, login, host, commit, build, device string) {
ws.SetReadLimit(readLimit)
c := &conn{ws: ws, done: make(chan struct{}), pending: map[string]chan api.ClientReply{}}
r.mu.Lock()
now := time.Now().UTC()
e := r.entry(id, login, host, commit, now)
e := r.entry(id, login, host, commit, build, now)
old := e.conn
e.conn = c
e.Device = device
e.ConnectedAt = now
r.mu.Unlock()
if old != nil {
old.ws.Close(websocket.StatusGoingAway, "replaced by a newer connection from this client")
}
slog.Info("client connected", "id", id, "host", host, "login", login, "commit", commit)
slog.Info("client connected", "id", id, "host", host, "device", device, "login", login, "build", build, "commit", commit)
r.changed()
defer func() {
@@ -219,6 +223,16 @@ func (r *Registry) Recent() []Client {
return rows
}
func (r *Registry) RecentMacs() []Client {
out := []Client{}
for _, c := range r.Recent() {
if c.Device == "Mac" {
out = append(out, c)
}
}
return out
}
func (r *Registry) Call(ctx context.Context, name string, req api.ClientRequest) (api.ClientReply, error) {
r.mu.Lock()
matches := []entry{}
+9 -38
View File
@@ -11,7 +11,6 @@ import (
"io"
"log/slog"
"os"
"regexp"
"strconv"
"sync"
"sync/atomic"
@@ -33,7 +32,6 @@ type Options struct {
Reminder string
MCPServers []*mcp.Server
StdioMCP []mcp.Stdio
Transcript io.Writer
OnBadLine func(line []byte, reason string)
}
@@ -88,9 +86,7 @@ type Session struct {
events chan Event
done chan struct{}
writeLock chan struct{}
recordMu sync.Mutex
transcript io.Writer
writeLock chan struct{}
nextID atomic.Int64
pendingMu sync.Mutex
@@ -181,15 +177,14 @@ func spawn(opts Options) (*Session, error) {
func newSession(opts Options, stdin *os.File, stdout io.Reader) *Session {
s := &Session{
stdin: stdin,
events: make(chan Event, 256),
writeLock: make(chan struct{}, 1),
done: make(chan struct{}),
transcript: opts.Transcript,
pending: map[int64]chan rpcResult{},
servers: map[string]*mcp.Server{},
reminder: opts.Reminder,
onBadLine: opts.OnBadLine,
stdin: stdin,
events: make(chan Event, 256),
writeLock: make(chan struct{}, 1),
done: make(chan struct{}),
pending: map[int64]chan rpcResult{},
servers: map[string]*mcp.Server{},
reminder: opts.Reminder,
onBadLine: opts.OnBadLine,
}
for _, srv := range opts.MCPServers {
s.servers[srv.Name] = srv
@@ -552,8 +547,6 @@ func (s *Session) write(m message, deadline time.Time) error {
return os.ErrDeadlineExceeded
}
s.record("send", b)
err = s.stdin.SetWriteDeadline(deadline)
if err != nil {
return err
@@ -566,26 +559,6 @@ func (s *Session) write(m message, deadline time.Time) error {
return err
}
var emailField = regexp.MustCompile(`"email":"[^"]*"`)
func (s *Session) record(dir string, line []byte) {
if s.transcript == nil {
return
}
line = emailField.ReplaceAll(line, []byte(`"email":"[redacted]"`))
b, err := json.Marshal(map[string]any{"dir": dir, "msg": json.RawMessage(line)})
if err != nil {
slog.Error("record transcript", "err", err)
return
}
s.recordMu.Lock()
defer s.recordMu.Unlock()
s.transcript.Write(append(b, '\n'))
}
func (s *Session) readLoop(stdout io.Reader) {
defer close(s.done)
defer close(s.events)
@@ -620,8 +593,6 @@ func (s *Session) handleLine(line []byte) {
return
}
s.record("recv", line)
if m.Method == "" {
s.handleResponse(line, m)
return
+3 -3
View File
@@ -35,7 +35,7 @@ func (s *Server) ConnectClient(w http.ResponseWriter, r *http.Request) {
}
defer ws.CloseNow()
s.clients.Serve(r.Context(), ws, id, actor(r), host, r.Header.Get("Byre-Commit"))
s.clients.Serve(r.Context(), ws, id, actor(r), host, r.Header.Get("Byre-Commit"), r.Header.Get("Byre-Build"), r.Header.Get("Byre-Device"))
}
func (s *Server) SetClientSync(w http.ResponseWriter, r *http.Request) {
@@ -58,7 +58,7 @@ func (s *Server) SetClientSync(w http.ResponseWriter, r *http.Request) {
}
slog.Warn("client icloud sync failing", "id", id, "host", host, "error", *sync.Error)
}
s.clients.Synced(id, actor(r), host, r.Header.Get("Byre-Commit"), sync)
s.clients.Synced(id, actor(r), host, r.Header.Get("Byre-Commit"), r.Header.Get("Byre-Build"), sync)
s.clientsChanged()
w.WriteHeader(http.StatusNoContent)
}
@@ -84,7 +84,7 @@ func (s *Server) ExecClient(w http.ResponseWriter, r *http.Request) {
name = *req.Client
}
if name == "" {
recent := s.clients.Recent()
recent := s.clients.RecentMacs()
if len(recent) == 0 {
writeError(w, http.StatusServiceUnavailable, errors.New("no client Macs are connected"))
return
+3 -1
View File
@@ -200,7 +200,7 @@ func (s *Server) identify(next http.Handler) http.Handler {
}
func (s *Server) acted(login string, r *http.Request) {
if s.clients.Acted(r.Header.Get("Byre-Client"), login, r.Header.Get("Byre-Host"), r.Header.Get("Byre-Commit")) {
if s.clients.Acted(r.Header.Get("Byre-Client"), login, r.Header.Get("Byre-Host"), r.Header.Get("Byre-Commit"), r.Header.Get("Byre-Build")) {
s.clientsChanged()
}
}
@@ -212,7 +212,9 @@ func (s *Server) connectedClients() []api.ConnectedClient {
Id: c.ID,
Login: c.Login,
Host: c.Host,
Device: c.Device,
Commit: c.Commit,
Build: c.Build,
ConnectedAt: c.ConnectedAt,
LastActionAt: c.LastActionAt,
Sync: c.Sync,
-36
View File
@@ -129,42 +129,6 @@ func (s *Store) DeleteLeadInputs(ctx context.Context, ids []int64) error {
})
}
func (s *Store) AddLeadWake(ctx context.Context, owner, taskID string) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
active := false
err := tx.QueryRowContext(ctx, `SELECT status = ? FROM tasks WHERE id = ?`, string(api.TaskStatusActive), taskID).Scan(&active)
if errors.Is(err, sql.ErrNoRows) || (err == nil && !active) {
return ErrNotFound
}
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `INSERT OR IGNORE INTO lead_wakes (owner, task_id, created_at) VALUES (?, ?, ?)`, owner, taskID, now())
return err
})
}
func (s *Store) TakeLeadWakes(ctx context.Context, taskID string) ([]string, error) {
owners := []string{}
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
rows, err := tx.QueryContext(ctx, `DELETE FROM lead_wakes WHERE task_id = ? RETURNING owner`, taskID)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
owner := ""
err = rows.Scan(&owner)
if err != nil {
return err
}
owners = append(owners, owner)
}
return rows.Err()
})
return owners, err
}
func (s *Store) GetFocus(ctx context.Context, login string) (api.Focus, error) {
f := api.Focus{}
var task, project sql.NullString
@@ -0,0 +1 @@
DROP TABLE lead_wakes;
+2
View File
@@ -116,8 +116,10 @@ final class API: Sendable {
self.origin = origin
let config = URLSessionConfiguration.default
config.httpAdditionalHeaders = [
"Byre-Build": Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String,
"Byre-Client": clientID,
"Byre-Commit": Bundle.main.object(forInfoDictionaryKey: "ByreCommit") as! String,
"Byre-Device": deviceKind,
"Byre-Host": deviceName,
]
config.proxyConfigurations = [tailnetNode.proxy()]
+28 -17
View File
@@ -84,6 +84,34 @@ struct OtherEntry: View {
var body: some View {
let focused = focus.wrappedValue == otherStop
HStack(alignment: .bottom, spacing: 8) {
entry(focused)
#if os(iOS)
ComposerMenu(key: key, photos: true)
.disabled(busy)
#endif
}
.onAppear {
text = store.drafts?.textAnswer(key) ?? ""
}
.onChange(of: text) {
if text != store.drafts?.textAnswer(key) ?? "" {
store.drafts?.setTextAnswer(key, text)
}
store.paneFocus.setDraft(key, !trimmed(text).isEmpty)
}
.onChange(of: store.drafts?.textAnswer(key)) { _, new in
if (new ?? "") != text {
text = new ?? ""
}
}
.onDisappear {
store.paneFocus.setDraft(key, false)
}
}
@ViewBuilder
private func entry(_ focused: Bool) -> some View {
Group {
if otherOpen(store, key) {
VStack(alignment: .leading, spacing: 6) {
@@ -135,23 +163,6 @@ struct OtherEntry: View {
.onHover { hover = $0 }
}
}
.onAppear {
text = store.drafts?.textAnswer(key) ?? ""
}
.onChange(of: text) {
if text != store.drafts?.textAnswer(key) ?? "" {
store.drafts?.setTextAnswer(key, text)
}
store.paneFocus.setDraft(key, !trimmed(text).isEmpty)
}
.onChange(of: store.drafts?.textAnswer(key)) { _, new in
if (new ?? "") != text {
text = new ?? ""
}
}
.onDisappear {
store.paneFocus.setDraft(key, false)
}
}
private func open() {
+22 -4
View File
@@ -267,12 +267,12 @@ extension View {
}
#if os(iOS)
private struct ComposerMenu: View {
struct ComposerMenu: View {
let key: String
let photos: Bool
let models: [String]
let selected: String
let choose: @MainActor @Sendable (String) -> Void
var models: [String] = []
var selected = ""
var choose: @MainActor @Sendable (String) -> Void = { _ in }
@Environment(Store.self) private var store
@Environment(Overlays.self) private var overlays
@ViewState private var picking = false
@@ -286,6 +286,11 @@ private struct ComposerMenu: View {
} label: {
Label("Attach photos", systemImage: "photo")
}
Button {
paste()
} label: {
Label("Paste image", systemImage: "doc.on.clipboard")
}
}
if !models.isEmpty {
Picker("Model for the next turn", selection: Binding(get: { selected }, set: choose)) {
@@ -316,6 +321,19 @@ private struct ComposerMenu: View {
}
}
}
private func paste() {
let images = UIPasteboard.general.images ?? []
if images.isEmpty {
overlays.showAlert("The clipboard has no image.")
return
}
for image in images {
if let data = image.jpegData(compressionQuality: 0.85) {
store.images.add(key, data, name: nil, overlays)
}
}
}
}
#endif
+2 -1
View File
@@ -35,7 +35,7 @@ struct OverlayAlert: Identifiable {
}
enum LeftTab {
case lead, notes, workflows, status, server, settings
case lead, status, notes, workflows, server, settings
}
@MainActor
@@ -51,6 +51,7 @@ final class Overlays {
var switcher: SwitcherModel?
var leftTab = LeftTab.lead
var leadFocus = 0
var projectFocus = false
@ObservationIgnored var pickerFrame: CGRect = .zero
@ObservationIgnored var menuHover = false
var paneFrame: CGRect = .zero
+3 -1
View File
@@ -439,7 +439,7 @@ private struct WebButton: View {
.font(Fonts.font(13, weight: primary ? 700 : 400))
.foregroundStyle(primary ? Palette.bg : danger ? Palette.danger : Palette.text)
.padding(.horizontal, 12)
.padding(.vertical, 4)
.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)
@@ -458,7 +458,9 @@ struct SmallLinkButtonStyle: ButtonStyle {
.font(Fonts.font(11))
.foregroundStyle(color)
.padding(.horizontal, 6)
.padding(.vertical, touchOnly ? 3 : 0)
.overlay(RoundedRectangle(cornerRadius: 4).stroke(border))
.padding(.vertical, touchOnly ? 5 : 0)
.opacity(enabled ? 1 : 0.45)
.contentShape(Rectangle())
}
+2 -4
View File
@@ -124,11 +124,9 @@ private struct NoteRow: View {
@ViewState private var frame: CGRect = .zero
var body: some View {
HStack(alignment: .firstTextBaseline, spacing: 12) {
Text(note.text)
.textSelection(.enabled)
HStack(alignment: .top, spacing: 12) {
MarkdownView(blocks: parseMarkdown(note.text))
.frame(maxWidth: .infinity, alignment: .leading)
.fixedSize(horizontal: false, vertical: true)
Button("Delete") {
Task {
if !(await overlays.showConfirm(["Delete this note? Your lead stops applying it.", note.text], action: "Delete", danger: true, anchor: frame)) {
+11
View File
@@ -278,6 +278,17 @@ final class Pages {
}
return png
}
#else
func capture(_ view: WKWebView) async throws -> Data {
guard view.window != nil, !view.isHidden else {
throw ApiError(status: 0, message: "the tab isn't on screen: the human has the page view closed or another tab showing")
}
let image = try await view.takeSnapshot(configuration: nil)
guard let png = image.pngData() else {
throw ApiError(status: 0, message: "couldn't encode the capture as PNG")
}
return png
}
#endif
func close() {
+3 -3
View File
@@ -113,15 +113,15 @@ private struct LeftPane: View {
PaneTabButton(label: "Lead", active: tab == .lead) {
pick(.lead)
}
PaneTabButton(label: "Status", active: tab == .status) {
pick(.status)
}
PaneTabButton(label: "Notes", active: tab == .notes) {
pick(.notes)
}
PaneTabButton(label: "Workflows", active: tab == .workflows) {
pick(.workflows)
}
PaneTabButton(label: "Status", active: tab == .status) {
pick(.status)
}
PaneTabButton(label: "Server", active: tab == .server) {
pick(.server)
}
+80 -23
View File
@@ -28,14 +28,26 @@ private enum PhoneRoute: Hashable {
case task
}
private struct PhonePage: Equatable {
var tab: PhoneTab
var depth: Int
var task: String?
var taskTab: TaskTab?
}
@MainActor
private func taskTab(_ store: Store) -> TaskTab {
store.sheet.flatMap { $0.owner == .lead ? nil : $0.tab } ?? store.primaryTab
}
private enum MoreRoute: Hashable {
case notes, workflows, status, settings
case status, notes, workflows, settings
}
private let moreTitles: [MoreRoute: String] = [
.status: "Status",
.notes: "Notes",
.workflows: "Workflows",
.status: "Status",
.settings: "Settings",
]
@@ -65,23 +77,9 @@ struct PhoneView: View {
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
HStack(spacing: 4) {
tabButton("Lead", .lead)
HStack(spacing: 0) {
tabButton("Tasks", .tasks)
if !store.inbox.isEmpty {
Badge(count: store.inbox.count)
}
}
tabButton("More", .more)
Spacer(minLength: 0)
ConnectionMark()
}
.padding(.horizontal, 12)
.overlay(alignment: .top) {
Rectangle().fill(Palette.border).frame(height: 1)
}
tabBar
}
.scrollDismissesKeyboard(.interactively)
.overlay {
OverlayHost()
}
@@ -109,6 +107,9 @@ struct PhoneView: View {
.onChange(of: tab == .tasks && !path.isEmpty, initial: true) { _, shown in
store.paneFocus.setShown(shown)
}
.onChange(of: page) {
dismissKeyboard()
}
.fullScreenCover(isPresented: Binding(get: { zoomed != nil }, set: { shown in
if !shown {
store.sheet = nil
@@ -131,6 +132,51 @@ struct PhoneView: View {
}
}
private var tabBar: some View {
HStack(spacing: 4) {
tabButton("Lead", .lead)
HStack(spacing: 0) {
tabButton("Tasks", .tasks)
if !store.inbox.isEmpty {
Badge(count: store.inbox.count)
}
}
tabButton("More", .more)
Spacer(minLength: 0)
ConnectionMark()
Button {
overlays.openSwitcher(store)
} label: {
Image(systemName: "plus")
.font(Fonts.font(17, weight: 600))
.foregroundStyle(Palette.accent)
.padding(.horizontal, 10)
.padding(.vertical, 8)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityLabel("New task")
Button {
tab = .tasks
path = []
} label: {
Image(systemName: "list.bullet")
.font(Fonts.font(17, weight: 600))
.foregroundStyle(Palette.accent)
.padding(.horizontal, 10)
.padding(.vertical, 8)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityLabel("Task list")
}
.padding(.horizontal, 12)
.padding(.vertical, 4)
.overlay(alignment: .top) {
Rectangle().fill(Palette.border).frame(height: 1)
}
}
private func shown(_ which: PhoneTab, @ViewBuilder _ content: () -> some View) -> some View {
content()
.opacity(tab == which ? 1 : 0)
@@ -147,6 +193,13 @@ struct PhoneView: View {
}
}
private var page: PhonePage {
if tab != .tasks || path.isEmpty {
return PhonePage(tab: tab, depth: path.count)
}
return PhonePage(tab: tab, depth: path.count, task: store.selectedTaskId, taskTab: taskTab(store))
}
private var zoomed: SheetView? {
switch store.sheet?.view {
case .image, .diagram: store.sheet?.view
@@ -199,8 +252,6 @@ private struct ConnectionMark: View {
Circle()
.fill(store.connected ? Palette.ok : Palette.muted)
.frame(width: 8, height: 8)
Text(store.me?.displayName ?? "")
.foregroundStyle(Palette.muted)
}
.lineLimit(1)
}
@@ -280,7 +331,7 @@ private struct PhoneTask: View {
var body: some View {
let selected = store.selectedTaskId.flatMap { store.findTask($0) }
let accent = selected.map { taskPalette[$0.color] } ?? Palette.accent
let tab = store.sheet.flatMap { $0.owner == .lead ? nil : $0.tab } ?? store.primaryTab
let tab = taskTab(store)
VStack(spacing: 0) {
TaskBar()
.overlay(alignment: .leading) {
@@ -311,15 +362,21 @@ private struct PhoneTask: View {
}
}
@MainActor
private func dismissKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
private struct MoreScreen: View {
@ViewState private var route = MoreRoute.notes
@ViewState private var route = MoreRoute.status
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 4) {
ForEach([MoreRoute.notes, .workflows, .status, .settings], id: \.self) { r in
ForEach([MoreRoute.status, .notes, .workflows, .settings], id: \.self) { r in
PaneTabButton(label: moreTitles[r]!, active: route == r) {
route = r
dismissKeyboard()
}
}
Spacer(minLength: 0)
+13
View File
@@ -8,8 +8,10 @@ private let refreshKinds: Set<String> = ["task.merged", "project.created"]
struct ProjectView: View {
@Environment(Store.self) private var store
@Environment(Overlays.self) private var overlays
@ViewState private var todos: [String: Result<Todo, ApiError>] = [:]
@ViewState private var off: (() -> Void)?
@FocusState private var focused: Bool
var body: some View {
ScrollView {
@@ -23,6 +25,17 @@ struct ProjectView: View {
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.focusable()
.focusEffectDisabled()
.focused($focused)
.onChange(of: overlays.projectFocus, initial: true) {
if overlays.projectFocus {
overlays.projectFocus = false
DispatchQueue.main.async {
focused = true
}
}
}
.onAppear {
off = store.events.add { ev in
if refreshKinds.contains(ev.kind), let projectId = ev.projectId {
+12 -2
View File
@@ -7,12 +7,13 @@ typealias ClientRequest = Components.Schemas.ClientRequest
typealias ClientReply = Components.Schemas.ClientReply
typealias ClientSync = Components.Schemas.ClientSync
#if os(macOS)
private let retryAfter = Duration.seconds(2)
private let checkEvery = Duration.seconds(10)
private let staleAfter: TimeInterval = 25
private let maxOutput = 64 << 10
private let maxMessage = 256 << 20
#if os(macOS)
private let maxOutput = 64 << 10
private let maxRead = 64 << 20
private let copyTimeoutMs = 10 * 60 * 1000
private let hands = Hands()
@@ -27,6 +28,7 @@ private let signScript = """
esac
exec "$program" --status-fd=2 -bsau "$(git config user.signingkey || echo "$1")" >"$2"
"""
#endif
@MainActor
final class Remote {
@@ -157,6 +159,10 @@ final class Remote {
private func perform(_ req: ClientRequest) async throws -> ClientReply {
switch req.op {
#if os(iOS)
case .bash, .exec, .upload, .download, .readFile, .writeFile, .hands, .sign:
throw ApiError(status: 0, message: "an iPhone only shows and drives pages and files in byre's app (show_page, show_file, drive_page)")
#else
case .bash:
let home = FileManager.default.homeDirectoryForCurrentUser.path
let (code, output) = try await spawn(["/bin/zsh", "-l", "-c", req.command ?? ""], in: home, timeoutMs: req.timeoutMs ?? 120_000)
@@ -190,6 +196,7 @@ final class Remote {
case .sign:
let (code, status, signature) = try await sign(Data(req.data?.data ?? []), as: req.ident ?? "", timeoutMs: req.timeoutMs ?? 120_000)
return ClientReply(id: req.id, exitCode: code, output: status, data: Base64EncodedData(signature))
#endif
case .openPage:
if let address = req.url {
guard let url = URL(string: address) else {
@@ -219,6 +226,7 @@ final class Remote {
}
}
#if os(macOS)
private func sign(_ payload: Data, as ident: String, timeoutMs: Int) async throws -> (Int, String, Data) {
let fm = FileManager.default
let dir = fm.temporaryDirectory.appending(path: "byre-sign-\(UUID().uuidString)", directoryHint: .isDirectory)
@@ -293,8 +301,10 @@ final class Remote {
}
try fm.moveItem(at: entries[0], to: target)
}
#endif
}
#if os(macOS)
private func expand(_ path: String) -> URL {
URL(filePath: (path as NSString).expandingTildeInPath)
}
+1 -1
View File
@@ -83,7 +83,7 @@ struct ChecksView: View {
case "required":
return "Required"
case "agent":
return "One of claude, codex and agy required"
return "One of claude, codex or agy required"
default:
return "Optional"
}
-6
View File
@@ -254,13 +254,7 @@ final class Connection {
store.startGlobalStream(after: status.lastEventSeq) { [weak self] in
self?.reconnected(status.startedAt)
}
#if os(macOS)
store.remote.start()
#else
if let sync = Servers.shared.sync {
store.reportSync(sync)
}
#endif
forwards.sync(store.api, Set(store.activeTasks().map(\.task.id)))
Notifier.shared.deliver()
return true
+58 -19
View File
@@ -3,6 +3,7 @@ import SwiftUI
typealias AgentProcess = Components.Schemas.AgentProcess
typealias Zombie = Components.Schemas.Zombie
typealias ConnectedClient = Components.Schemas.ConnectedClient
typealias VendorUsage = Components.Schemas.Usage
typealias UsageWindow = Components.Schemas.UsageWindow
@@ -440,28 +441,66 @@ private struct ClientTable: View {
if status.clients.isEmpty {
Text("No clients connected.").foregroundStyle(Palette.muted)
} else {
Grid(alignment: .leading, horizontalSpacing: 12, verticalSpacing: 2) {
GridRow {
ForEach(["Mac", "User", "Build", "Connected", "Last action", "iCloud"], id: \.self) { h in
Text(h).font(Fonts.font(11)).foregroundStyle(Palette.muted)
}
}
Divider().overlay(Palette.border)
ForEach(Array(status.clients.enumerated()), id: \.offset) { _, c in
GridRow {
Text(c.host.isEmpty ? "—" : c.host)
Text(c.login)
Text(c.commit.isEmpty ? "—" : String(c.commit.prefix(8))).help(c.commit)
Text(since(c.connectedAt))
Text(c.lastActionAt.map(since) ?? "—")
SyncCell(sync: c.sync)
}
.textSelection(.enabled)
Divider().overlay(Palette.border)
}
ViewThatFits(in: .horizontal) {
grid
stacked
}
}
}
private var grid: some View {
Grid(alignment: .leading, horizontalSpacing: 12, verticalSpacing: 2) {
GridRow {
ForEach(["Name", "Device", "User", "Build", "Connected", "Last action", "iCloud"], id: \.self) { h in
Text(h).font(Fonts.font(11)).foregroundStyle(Palette.muted)
}
}
Divider().overlay(Palette.border)
ForEach(Array(status.clients.enumerated()), id: \.offset) { _, c in
GridRow {
Text(c.host.isEmpty ? "—" : c.host).fixedSize()
Text(c.device.isEmpty ? "—" : c.device).fixedSize()
Text(c.login).fixedSize()
Text(build(c)).help(c.commit).fixedSize()
Text(ago(c.connectedAt)).fixedSize()
Text(c.lastActionAt.map(since) ?? "—").fixedSize()
SyncCell(sync: c.sync).fixedSize()
}
.textSelection(.enabled)
Divider().overlay(Palette.border)
}
}
}
private var stacked: some View {
VStack(alignment: .leading, spacing: 0) {
Divider().overlay(Palette.border)
ForEach(Array(status.clients.enumerated()), id: \.offset) { _, c in
VStack(alignment: .leading, spacing: 2) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(c.host.isEmpty ? "—" : c.host)
Text(c.device.isEmpty ? "—" : c.device).font(Fonts.font(11)).foregroundStyle(Palette.muted)
Spacer(minLength: 0)
Text("iCloud").font(Fonts.font(11)).foregroundStyle(Palette.muted)
SyncCell(sync: c.sync)
}
Text("\(c.login) · build \(build(c))")
.font(Fonts.font(11))
.foregroundStyle(Palette.muted)
Text("Connected \(ago(c.connectedAt)) ·last action \(c.lastActionAt.map(since) ?? "—")")
.font(Fonts.font(11))
.foregroundStyle(Palette.muted)
}
.textSelection(.enabled)
.padding(.vertical, 6)
Divider().overlay(Palette.border)
}
}
}
private func build(_ c: ConnectedClient) -> String {
c.build.isEmpty ? "—" : c.build
}
}
private struct SyncCell: View {
-6
View File
@@ -89,9 +89,7 @@ final class Store {
let imageCache = ImageCache()
let events = Listeners<ByreEvent>()
let paneFocus: PaneFocus
#if os(macOS)
let remote: Remote
#endif
let forwards: Forwards
let pages: Pages
@@ -115,10 +113,8 @@ final class Store {
self.pages = pages
leadFeed = Feed(shown: { true })
paneFocus = PaneFocus(api: api)
#if os(macOS)
remote = Remote(api: api)
remote.store = self
#endif
pages.popup = { [weak self] page in
self?.openPage(page)
}
@@ -144,9 +140,7 @@ final class Store {
stopped = true
globalStream?.stop()
leadStream?.stop()
#if os(macOS)
remote.stop()
#endif
drafts?.flush()
await paneFocus.leave()
}
+5 -1
View File
@@ -19,6 +19,7 @@ func runGo(_ key: String, _ store: Store, _ overlays: Overlays) {
overlays.switcher?.close()
store.sheet = nil
if let project = store.focusedProject() ?? store.projects.first {
overlays.projectFocus = true
store.focusProject(project.id)
}
case "l":
@@ -279,6 +280,7 @@ final class SwitcherModel {
go(task.id)
case .project(let p):
close()
overlays.projectFocus = true
store.focusProject(p.id)
case .newTask(let p):
create(p.id, .init())
@@ -301,7 +303,9 @@ final class SwitcherModel {
if store.inbox.contains(where: { $0.taskId == taskId }) {
store.showQuestion(taskId)
} else {
store.composerFocus = taskId
if !touchOnly {
store.composerFocus = taskId
}
store.focusTask(taskId)
}
}
+33
View File
@@ -3,6 +3,9 @@ import Network
import Observation
import SwiftUI
import TailscaleKit
#if os(iOS)
import SafariServices
#endif
func slug(_ text: String) -> String {
var out = ""
@@ -288,6 +291,7 @@ private struct NodeLoginRow: View {
let role: String
let state: String
let authURL: URL?
@State private var signingIn = false
var body: some View {
HStack(alignment: .center, spacing: 12) {
@@ -307,9 +311,13 @@ private struct NodeLoginRow: View {
.frame(maxWidth: .infinity, alignment: .leading)
if state != "Running" {
Button("Sign in") {
#if os(macOS)
if let authURL {
openExternal(authURL)
}
#else
signingIn = true
#endif
}
.buttonStyle(WebButtonStyle(primary: true))
.disabled(authURL == nil)
@@ -319,5 +327,30 @@ private struct NodeLoginRow: View {
.padding(12)
.background(Palette.bg, in: RoundedRectangle(cornerRadius: 8))
.overlay(RoundedRectangle(cornerRadius: 8).stroke(Palette.border))
#if os(iOS)
.sheet(isPresented: $signingIn) {
if let authURL {
SafariView(url: authURL)
.ignoresSafeArea()
}
}
.onChange(of: state) {
if state == "Running" {
signingIn = false
}
}
#endif
}
}
#if os(iOS)
private struct SafariView: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: Context) -> SFSafariViewController {
SFSafariViewController(url: url)
}
func updateUIViewController(_ controller: SFSafariViewController, context: Context) {}
}
#endif
+5 -1
View File
@@ -1054,7 +1054,11 @@ private func toolTarget(_ input: (any Sendable)?, _ root: String) -> String {
guard let path = (i?["file_path"] ?? i?["path"]) as? String else {
return ""
}
return relative(path, root)
let short = relative(path, root)
if short != path || !path.hasPrefix("/") {
return short
}
return URL(fileURLWithPath: path).lastPathComponent
}
private func relative(_ path: String, _ root: String) -> String {
+1 -1
View File
@@ -49,7 +49,7 @@ ditto "$build/TailscaleKit.framework" "$app/Contents/Frameworks/TailscaleKit.fra
cp "$mac/Info.plist" "$app/Contents/Info.plist"
plutil -insert ByreCommit -string "$(git -C "$root" rev-parse HEAD)" "$app/Contents/Info.plist"
plutil -insert CFBundleShortVersionString -string "$version" "$app/Contents/Info.plist"
plutil -insert CFBundleVersion -string "$(git -C "$root" rev-list --count HEAD)" "$app/Contents/Info.plist"
plutil -insert CFBundleVersion -string "$(git -C "$root" log -1 --format=%ct)" "$app/Contents/Info.plist"
cp "$mac"/fonts/*.woff2 "$app/Contents/Resources/fonts/"
cp "$icns" "$app/Contents/Resources/byre.icns"
cp "$mac/byre.provisionprofile" "$app/Contents/embedded.provisionprofile"
+1
View File
@@ -8,4 +8,5 @@ ditto "$mount/Byre.app" /Applications/Byre.app.new
hdiutil detach "$mount"
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
+1 -1
View File
@@ -40,7 +40,7 @@ plutil -insert CFBundleIcons -xml "$(plutil -extract CFBundleIcons xml1 -o - "$b
plutil -insert ByreCommit -string "$(git -C "$root" rev-parse HEAD)" "$plist"
version=$(git -C "$root" describe --tags --abbrev=0)
plutil -insert CFBundleShortVersionString -string "${version#v}" "$plist"
plutil -insert CFBundleVersion -string "$(date -u +%Y%m%d.%H%M%S)" "$plist"
plutil -insert CFBundleVersion -string "$(git -C "$root" log -1 --format=%ct)" "$plist"
platform=$(xcrun --sdk "$sdk" --show-sdk-platform-version)
sdkbuild=$(xcrun --sdk "$sdk" --show-sdk-build-version)
plutil -insert CFBundleSupportedPlatforms -xml "<array><string>$(if [ "$sdk" = iphoneos ]; then echo iPhoneOS; else echo iPhoneSimulator; fi)</string></array>" "$plist"