diff --git a/api/openapi.yaml b/api/openapi.yaml index fea0e5e..d57709f 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -1365,7 +1365,7 @@ components: worktree_path: { type: string } base_ref: { type: string } port_base: { type: integer, description: "The first of the task's block of ports (port_base to port_base+9), passed to its agent and terminal as PORT." } - color: { type: integer, description: "Index into the UI's task palette, assigned at creation: a colour no active task holds, the one given out longest ago." } + color: { type: integer, description: "Index into the UI's task palette, assigned at creation: a color no active task holds, the one given out longest ago." } status: type: string enum: [active, merged, discarded] diff --git a/docs/macos.md b/docs/macos.md index edea3c4..22c382d 100644 --- a/docs/macos.md +++ b/docs/macos.md @@ -37,7 +37,7 @@ Every build is signed for distribution outside the App Store: the team's Develop ## Views - The transcript is a plain `VStack` in a `ScrollView`, not lazy: a `LazyVStack` estimates the height of rows it hasn't shown yet and corrects it when they scroll into view, without moving the scroll offset, so scrolling up through rows not yet shown jumped by each correction; SwiftUI can't move the offset mid-scroll without stopping its momentum. -- The terminal cursor is a steady block: SwiftTerm's blinking cursor restarts its Core Animation fade on every keypress, scheduled 0.7 s out, and on a ProMotion display macOS then holds the window's screen update until that fade starts, so each typed letter appears about 0.7 s late. The terminal's colours are set once per light/dark scheme, since each colour set makes SwiftTerm redraw the whole screen. +- The terminal cursor is a steady block: SwiftTerm's blinking cursor restarts its Core Animation fade on every keypress, scheduled 0.7 s out, and on a ProMotion display macOS then holds the window's screen update until that fade starts, so each typed letter appears about 0.7 s late. The terminal's colors are set once per light/dark scheme, since each color set makes SwiftTerm redraw the whole screen. ## Updates diff --git a/docs/operations.md b/docs/operations.md index 8b3976b..d5b9a0f 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -120,7 +120,7 @@ Task agents use the Macs the human has byre's app open on, through the app's Web `byre-hands` (`hands/`) is the MCP server that lets agents see and drive a Mac. Its tool descriptions in `hands/Sources/byre-hands/Tools.swift` are what agents read. -- **One path per process.** replayd, the daemon behind ScreenCaptureKit, keys its clients by executable path: two processes run from one path keep cancelling each other's connection, and every capture from that path hangs. replayd's log shows it as `accepted client connection PID: ` and `invalidated because the current process cancelled the connection` alternating between two pids, thousands of times a second. So each server session runs its own copy of `byre-hands` from its own file, and the app runs one shared by all its connections. +- **One path per process.** replayd, the daemon behind ScreenCaptureKit, keys its clients by executable path: two processes run from one path keep canceling each other's connection, and every capture from that path hangs. replayd's log shows it as `accepted client connection PID: ` and `invalidated because the current process cancelled the connection` alternating between two pids, thousands of times a second. So each server session runs its own copy of `byre-hands` from its own file, and the app runs one shared by all its connections. - **Window server.** `NSApplication.shared` connects the process to the window server at startup, without which ScreenCaptureKit asserts. AppleScript needs the main thread with a run loop, so every script runs there. - **Busy apps.** An app in a menu-tracking loop or otherwise busy answers neither Apple events nor accessibility requests; scripts time out after 10s and accessibility requests after 3s, and the first timeout fails the whole call rather than waiting on every element. - **Display sleep.** While the display sleeps, ScreenCaptureKit lists no displays. Every call holds a `PreventUserIdleDisplaySleep` assertion and, when the display is asleep, declares user activity and waits for ScreenCaptureKit to list it again. A declared activity doesn't reset the keyboard and mouse idle time, so the display sleeps again after powerd's grace period (`com.apple.powermanagement.delayDisplayOff` in `pmset -g assertions`), not after the full display-sleep delay. `pmset -g log` shows each wake as `Display is turned on` beside `byre-hands` assertions. diff --git a/internal/agents/hands.go b/internal/agents/hands.go index f448c9b..995bb19 100644 --- a/internal/agents/hands.go +++ b/internal/agents/hands.go @@ -25,7 +25,7 @@ func (m *Manager) handsDir(sessionID string) string { } // replayd keys ScreenCaptureKit clients by executable path, so two hands -// processes run from one path keep cancelling each other's connection. +// processes run from one path keep canceling each other's connection. func (m *Manager) handsMCP(sessionID, mediaDir string) (mcp.Stdio, error) { dir := m.handsDir(sessionID) err := os.RemoveAll(dir) diff --git a/internal/agents/inbox.go b/internal/agents/inbox.go index 7be6bb7..187bc7b 100644 --- a/internal/agents/inbox.go +++ b/internal/agents/inbox.go @@ -231,7 +231,7 @@ func (m *Manager) cancelQuestion(ctx context.Context, l *live, raw json.RawMessa err = m.store.WithdrawItem(ctx, item.Id) if errors.Is(err, store.ErrNotOpen) { - return "", fmt.Errorf("question %s is already %s; nothing was cancelled", item.Id, item.State) + return "", fmt.Errorf("question %s is already %s; nothing was canceled", item.Id, item.State) } if err != nil { return "", err diff --git a/internal/agents/lead.go b/internal/agents/lead.go index fe70ea8..c60b1bb 100644 --- a/internal/agents/lead.go +++ b/internal/agents/lead.go @@ -14,7 +14,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 cancelled or dropped), starting to merge, merged or discarded, and every change of a task's state, as " state: → ." 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. The states are running, idle, 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. 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 " state: → ." 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. The states are running, idle, 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. 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 on the task's transcript tab, 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's project tab, 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//, for example [Fix login redirect](#/todo/3f2a9c1b7d4e/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///<brief>, both URL-encoded with parentheses as %28 and %29, for example [Lead tool for admin status](#/suggest/3f2a9c1b7d4e/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. @@ -33,7 +33,7 @@ const leadGuidance = `You are the human's lead agent in byre, shown in the left - You can create tasks, start TODO items, message, interrupt and compact task agents, and merge or discard tasks. Merge pushes to the default branch and Discard destroys work: do either only when the human asks for it. - Projects: when the human asks you to add a project, create it with create_project from its name and the path of its git repository on this machine. - Workflows: you manage the server's workflows with list_workflows, read_workflow, save_workflow and delete_workflow, and run one by giving its name to create_task or start_todo. A workflow is a tree of steps run in order, each a stage, an order or a parallel; every stage runs as its own task on the stage's model once the step before it has finished, so stages fan out (a parallel after a stage) and fan in (a stage after a parallel); a stage's agent calls finish_stage to hand its output to the stages after it, signals tell you when a stage starts and when the workflow finishes, and the workflow finishes once every stage has; byre then discards the run's other stage tasks, leaving the task of each last stage (one per branch when it ends in a parallel) to merge. Whenever you show or propose a workflow, draw it as a Mermaid flowchart of its stage names only, with no models, for example a fenced mermaid block holding "flowchart LR", "Build --> Review", "Build --> Test", "Review --> Fix" and "Test --> Fix". -- Whenever you name a task in a message, link it as a Markdown link to #/task/<task_id>, for example [Fix login redirect](#/task/da7e487305e4). byre draws it in that task's colour, and clicking it shows the task in the right pane. To open a specific tab, append it: #/task/<task_id>/<tab>. When you point the human at a question to answer or a finished turn, link to #/task/<task_id>/transcript. +- Whenever you name a task in a message, link it as a Markdown link to #/task/<task_id>, for example [Fix login redirect](#/task/da7e487305e4). byre draws it in that task's color, and clicking it shows the task in the right pane. To open a specific tab, append it: #/task/<task_id>/<tab>. When you point the human at a question to answer or a finished turn, link to #/task/<task_id>/transcript. - Keep messages short; the human reads them in half the screen. Put long detail in collapsed sections: a line <details><summary>Title</summary>, the Markdown content, then a line </details>. - You are long-running. Your context window is large. byre sends you a signal once your context passes a quarter of its window; then call compact_self at a logical point (a piece of work wrapped up, a topic closed), and not before; when the turn ends, byre replaces you with a fresh session that starts from your handoff. - Don't commit, push, or touch git branches yourself.` @@ -596,7 +596,7 @@ func (m *Manager) signalText(ctx context.Context, ev api.Event) (string, error) return fmt.Sprintf("%s was set by %s to merge once the reviews of snapshot %v finish with no findings.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor, ev.Payload["snapshot_n"]), nil case "task.merge_after_review_cancelled": - return fmt.Sprintf("%s's merge after review was cancelled by %s.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor), nil + return fmt.Sprintf("%s's merge after review was canceled by %s.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor), nil case "task.merge_after_review_dropped": return fmt.Sprintf("%s didn't merge after the review: %v", m.taskLabel(ctx, deref(ev.TaskId)), ev.Payload["reason"]), nil diff --git a/internal/agents/leadtools.go b/internal/agents/leadtools.go index f1b53c1..5cab9c8 100644 --- a/internal/agents/leadtools.go +++ b/internal/agents/leadtools.go @@ -1089,7 +1089,7 @@ func transcriptLine(ev api.Event, root string) string { case "task.merge_after_review": return fmt.Sprintf("set by %s to merge after the review of snapshot %s", ev.Actor, text("snapshot_n")) case "task.merge_after_review_cancelled": - return fmt.Sprintf("merge after review cancelled by %s", ev.Actor) + return fmt.Sprintf("merge after review canceled by %s", ev.Actor) case "task.merge_after_review_dropped": return "didn't merge after the review: " + clip(text("reason"), 300) case "task.merged", "task.discarded": diff --git a/internal/agents/terminal.go b/internal/agents/terminal.go index 885a87a..76384e9 100644 --- a/internal/agents/terminal.go +++ b/internal/agents/terminal.go @@ -165,7 +165,7 @@ func (m *Manager) terminalTypeTool(l *live) mcp.Tool { func (m *Manager) wakeOnPrompt(l *live) func(string, []byte) { return func(exit string, out []byte) { - text := fmt.Sprintf("The line you left unsent in the terminal was run, and the shell is back at its prompt with exit status %s (130 means it was cancelled with Ctrl-C). The terminal's output since you typed it:\n\n%s", exit, terminal.Text(out)) + text := fmt.Sprintf("The line you left unsent in the terminal was run, and the shell is back at its prompt with exit status %s (130 means it was canceled with Ctrl-C). The terminal's output since you typed it:\n\n%s", exit, terminal.Text(out)) _, _, err := m.Message(context.Background(), l.id, "byre", text, "", "") if err != nil { slog.Error("wake agent on terminal prompt", "session", l.id, "task", l.taskID, "err", err) diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index 89f29f8..ca777b9 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -1301,7 +1301,7 @@ type Task struct { // ClosedAt When the task was merged or discarded. ClosedAt *time.Time `json:"closed_at,omitempty"` - // Color Index into the UI's task palette, assigned at creation: a colour no active task holds, the one given out longest ago. + // Color Index into the UI's task palette, assigned at creation: a color no active task holds, the one given out longest ago. Color int `json:"color"` CreatedAt time.Time `json:"created_at"` CreatedBy string `json:"created_by"` diff --git a/mac/Sources/Byre/Chat.swift b/mac/Sources/Byre/Chat.swift index 44796d7..4c1d797 100644 --- a/mac/Sources/Byre/Chat.swift +++ b/mac/Sources/Byre/Chat.swift @@ -1657,7 +1657,7 @@ struct NoteView: View { let cancelVisible: Bool @Environment(Store.self) private var store @Environment(Overlays.self) private var overlays - @ViewState private var cancelling = false + @ViewState private var canceling = false var body: some View { let color = switch note.style { @@ -1679,18 +1679,18 @@ struct NoteView: View { } if let taskId = note.cancelMergeAfterReview, cancelVisible, store.findTask(taskId)?.mergeAfterReview == true { Button("Cancel") { - cancelling = true + canceling = true Task { do { _ = try await store.api.client.cancelMergeAfterReview(path: .init(taskId: taskId)).ok } catch { overlays.showAlert(errorText(error)) } - cancelling = false + canceling = false } } .buttonStyle(SmallLinkButtonStyle()) - .disabled(cancelling) + .disabled(canceling) .help("Withdraw this request; nothing is merged") } } diff --git a/mac/Sources/Byre/Settings.swift b/mac/Sources/Byre/Settings.swift index 29eb8b0..93d02af 100644 --- a/mac/Sources/Byre/Settings.swift +++ b/mac/Sources/Byre/Settings.swift @@ -22,8 +22,8 @@ struct SettingsView: View { var body: some View { ScrollView { VStack(alignment: .leading, spacing: 0) { - heading("Window colour") - Text("Windows for this server have a border in this colour, on every Mac you use.") + heading("Window color") + Text("Windows for this server have a border in this color, on every Mac you use.") .foregroundStyle(Palette.muted) .fixedSize(horizontal: false, vertical: true) ServerColorPicker() @@ -118,7 +118,7 @@ private struct SyncState: View { TimelineView(.periodic(from: .now, by: 30)) { _ in if let sync = Servers.shared.sync { if sync.ok { - Text("This Mac shares its servers, window colours, font size, drafts and task visits through iCloud. Last synced \(ago(sync.at)).") + Text("This Mac shares its servers, window colors, font size, drafts and task visits through iCloud. Last synced \(ago(sync.at)).") .fixedSize(horizontal: false, vertical: true) } else { Text("iCloud sync is failing on this Mac, so changes made here aren't reaching your other Macs. Failed \(ago(sync.at)): \(sync.error ?? "")") diff --git a/mac/Sources/Byre/Terminal.swift b/mac/Sources/Byre/Terminal.swift index c1fe676..cd33730 100644 --- a/mac/Sources/Byre/Terminal.swift +++ b/mac/Sources/Byre/Terminal.swift @@ -99,11 +99,11 @@ struct TerminalHost: NSViewRepresentable { } context.coordinator.scheme = scheme DispatchQueue.main.async { - colour(nsView) + color(nsView) } } - private func colour(_ t: TerminalView) { + private func color(_ t: TerminalView) { func resolved(_ c: SwiftUI.Color) -> NSColor { var out = NSColor(c) t.effectiveAppearance.performAsCurrentDrawingAppearance { diff --git a/mac/Sources/Byre/TranscriptBuilder.swift b/mac/Sources/Byre/TranscriptBuilder.swift index 677b474..3e90fee 100644 --- a/mac/Sources/Byre/TranscriptBuilder.swift +++ b/mac/Sources/Byre/TranscriptBuilder.swift @@ -949,7 +949,7 @@ final class TranscriptBuilder { return Row(.note(n), taskId: rowTask) case "task.merge_after_review_cancelled": - return note([.text("\(ev.actor) cancelled the merge after review")]) + return note([.text("\(ev.actor) canceled the merge after review")]) case "task.merge_after_review_dropped": return note([.text("didn't merge after the review: \(jsString(p["reason"]))")]) diff --git a/mac/Sources/Byre/Workflow.swift b/mac/Sources/Byre/Workflow.swift index da5e2b7..ab895a1 100644 --- a/mac/Sources/Byre/Workflow.swift +++ b/mac/Sources/Byre/Workflow.swift @@ -254,7 +254,7 @@ private struct AttemptRow: View { if let output = a.output { DetailsBox(boxed: false) { HStack(spacing: 0) { - Text("finish_stage output").foregroundStyle(Palette.muted) + Text("Stage output").foregroundStyle(Palette.muted) if let commit = a.commit { Text(" \(String(commit.prefix(10)))").foregroundStyle(Palette.muted) }