From ff8d4fb4be0b56173e707b01582604e0d5cab8ce Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Fri, 23 Oct 2026 15:17:27 -0700 Subject: [PATCH] Put macOS model and effort in a menu; take_snapshot commits the branch --- internal/agents/guidance.go | 2 +- internal/agents/leadtools.go | 2 +- internal/agents/snapshots.go | 10 ++++- mac/Sources/Byre/Composer.swift | 75 ++++++++++++++++----------------- 4 files changed, 47 insertions(+), 42 deletions(-) diff --git a/internal/agents/guidance.go b/internal/agents/guidance.go index 14be827..4b4fe90 100644 --- a/internal/agents/guidance.go +++ b/internal/agents/guidance.go @@ -19,7 +19,7 @@ const guidance = `You are running inside byre. The human reads your work in byre - End every turn with a short final message for the human: what you did, anything you departed from in the plan or instructions, risks, and what's next. byre shows it as the turn's summary at the end of the transcript. State every departure from the plan. - A turn that asks a question has no summary; put everything the human needs in the question's context. - In that final message and in ask_human context, put long supporting detail (logs, full lists, data) in collapsed sections the human expands on demand: a line
Title, the Markdown content, then a line
. -- Don't commit. byre commits everything in the worktree at the end of each turn, using the turn's final message. +- Don't commit with git. byre commits everything in the worktree at the end of each turn, using the turn's final message. When you need a commit mid-turn (e.g. to merge from main), call take_snapshot. - Don't push, don't switch or touch other branches, and don't run git maintenance (gc, prune, worktree prune). byre handles merging and pushing. - TODO.md at the repo root is the project's list of work for byre; its format is the read_knowledge article todo-file.` diff --git a/internal/agents/leadtools.go b/internal/agents/leadtools.go index 8d0240a..a8dbc5d 100644 --- a/internal/agents/leadtools.go +++ b/internal/agents/leadtools.go @@ -468,7 +468,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool { if err != nil { return "", err } - return fmt.Sprintf("Snapshot of %s requested; its review shows in the task's transcript, and findings go to its agent.", task.Id), nil + return fmt.Sprintf("%s's worktree committed to its branch, and snapshot requested; its review shows in the task's transcript, and findings go to its agent.", task.Id), nil }), tool("discard_task", diff --git a/internal/agents/snapshots.go b/internal/agents/snapshots.go index c064e96..7f529fd 100644 --- a/internal/agents/snapshots.go +++ b/internal/agents/snapshots.go @@ -205,6 +205,12 @@ func (m *Manager) RequestSnapshot(ctx context.Context, taskID string) error { if err != nil { return err } + unlock := m.lockTask(task.Id) + _, err = git.CommitAll(ctx, t.dir, "Work before take_snapshot", t.gitName, t.gitEmail) + unlock() + if err != nil { + return err + } err = m.store.BeginReviewRequest(ctx, task.Id) if err != nil { return err @@ -263,12 +269,12 @@ func (m *Manager) takeSnapshotTool(l *live) mcp.Tool { if err != nil { return "", err } - return "Snapshot requested. If the project has a review workflow, its reviewers check it against the default branch; their findings come to you together once the workflow finishes.", nil + return "Worktree committed to the task's branch, and snapshot requested. If the project has a review workflow, its reviewers check it against the default branch; their findings come to you together once the workflow finishes.", nil }, } } -const takeSnapshotDescription = "Snapshot a task's worktree now: byre commits the whole worktree, uncommitted work included, as a commit on byre's own ref for the task (refs/byre/tasks//snap/), on top of the branch's HEAD. The task's branch and worktree are left as they are; the branch gets its own commit at the end of the turn. If the project has a review workflow, its reviewers check all of the task's work against the default branch, using each stage's model and instructions; findings arrive together once the workflow finishes. " + +const takeSnapshotDescription = "Commit and snapshot a task's worktree now: byre commits the whole worktree, uncommitted work included, to the task's branch, and returns once that commit is made. It then snapshots that commit on byre's own ref for the task (refs/byre/tasks//snap/). If the project has a review workflow, its reviewers check all of the task's work against the default branch, using each stage's model and instructions; findings arrive together once the workflow finishes. " + "byre already does this at the end of every turn that changes files; call this to review work mid-turn, or to retry a review that failed (it snapshots even when nothing changed since the last snapshot)." func (m *Manager) dropSnapshots(repo, taskID string) error { diff --git a/mac/Sources/Byre/Composer.swift b/mac/Sources/Byre/Composer.swift index c847a15..ef427ff 100644 --- a/mac/Sources/Byre/Composer.swift +++ b/mac/Sources/Byre/Composer.swift @@ -401,7 +401,6 @@ extension View { } } -#if os(iOS) struct ComposerMenu: View { let key: String let photos: Bool @@ -412,11 +411,35 @@ struct ComposerMenu: View { var chooseEffort: @MainActor @Sendable (String) -> Void = { _ in } @Environment(Store.self) private var store @Environment(Overlays.self) private var overlays + #if os(iOS) @ViewState private var picking = false @ViewState private var picked: [PhotosPickerItem] = [] + #endif var body: some View { + menu + #if os(iOS) + .photosPicker(isPresented: $picking, selection: $picked, matching: .images) + .onChange(of: picked) { + let items = picked + if items.isEmpty { + return + } + picked = [] + Task { + for item in items { + if let data = try? await item.loadTransferable(type: Data.self) { + store.images.add(key, data, name: nil, overlays) + } + } + } + } + #endif + } + + private var menu: some View { Menu { + #if os(iOS) if photos { Button { picking = true @@ -429,6 +452,7 @@ struct ComposerMenu: View { Label("Paste image", systemImage: "doc.on.clipboard") } } + #endif if !models.isEmpty { Picker("Model for the next turn", selection: Binding(get: { selected }, set: choose)) { ForEach(models, id: \.self) { option in @@ -447,25 +471,12 @@ struct ComposerMenu: View { Image(systemName: "ellipsis") .frame(height: 14) } + .menuStyle(.button) + .menuIndicator(.hidden) .buttonStyle(WebButtonStyle()) - .photosPicker(isPresented: $picking, selection: $picked, matching: .images) - .onChange(of: picked) { - let items = picked - if items.isEmpty { - return - } - picked = [] - Task { - for item in items { - if let data = try? await item.loadTransferable(type: Data.self) { - store.images.add(key, data, name: nil, overlays) - } - } - } - } + .fixedSize() } } -#endif struct Thumbnails: View { let key: String @@ -638,33 +649,21 @@ private struct ComposerBar: View, Equatable { } .buttonStyle(WebButtonStyle()) .help(shown ? "Hide the message box" : "Show the message box") - #if os(iOS) - ComposerMenu(key: draftKey, photos: shown, models: chat.team ? [] : options, selected: selected, effort: selectedEffort, choose: choose, chooseEffort: chooseEffort) - #else + #if os(macOS) if !chat.team { - Picker("", selection: Binding(get: { selected }, set: choose)) { - ForEach(options, id: \.self) { option in - Text(option).tag(option) - } - } - .labelsHidden() - .pickerStyle(.menu) - .fixedSize() - .help("Model for the next turn") - Picker("", selection: Binding(get: { selectedEffort }, set: chooseEffort)) { - ForEach(efforts(selected), id: \.self) { option in - Text(effortLabel(option)).tag(option) - } - } - .labelsHidden() - .pickerStyle(.menu) - .fixedSize() - .help("Effort for the next turn") + menu + .help("Model and effort for the next turn") } + #else + menu #endif } } + private var menu: some View { + ComposerMenu(key: draftKey, photos: shown, models: chat.team ? [] : options, selected: selected, effort: selectedEffort, choose: choose, chooseEffort: chooseEffort) + } + private var actions: some View { HStack(spacing: 8) { Button {