Open merge dialog instantly from pushed merge facts

This commit is contained in:
Ian Gulliver
2026-10-21 11:48:21 -07:00
parent 4bb6f1fa11
commit 890fb6b691
15 changed files with 736 additions and 678 deletions
+1
View File
@@ -369,6 +369,7 @@ func (m *Manager) Observe(events []api.Event) {
if to, _ := ev.Payload["to"].(string); ev.Kind == "task.state" && readyState(api.TaskState(to)) && ev.TaskId != nil {
go m.promptReady(*ev.TaskId, ev.Payload["from"])
}
m.observeFacts(ev)
switch {
case (ev.Kind == "task.merged" || ev.Kind == "task.merged_to_main" || ev.Kind == "task.discarded") && ev.ProjectId != nil:
go m.audit(*ev.ProjectId)
+10
View File
@@ -76,6 +76,11 @@ type Manager struct {
pokes chan struct{}
factsMu sync.Mutex
factsRun sync.Mutex
factsDirty map[string]bool
factsPoke chan struct{}
reviewMu sync.Mutex
usageMu map[api.AgentSessionVendor]*sync.Mutex
@@ -169,11 +174,14 @@ func New(st *store.Store, worktreeRoot, leadRoot, handsRoot, codexHome, antigrav
signals: make(chan []api.Event, 1024),
snaps: map[string]*snapState{},
pokes: make(chan struct{}, 1),
factsDirty: map[string]bool{},
factsPoke: make(chan struct{}, 1),
previewListeners: map[int]*previewListener{},
bad: newBadLines(),
watchStop: make(chan struct{}),
}
go m.signalLoop()
go m.factsLoop()
go m.loadUsage()
return m
}
@@ -469,6 +477,7 @@ func (m *Manager) Recover(ctx context.Context) error {
go m.deliverAllQueued(context.Background())
go m.coordinate()
m.poke()
m.MarkFacts(allTasks)
go m.recoverMergeAfterReviews(context.Background())
go m.auditAll(context.Background())
return nil
@@ -1232,6 +1241,7 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
}
}
}
m.refreshFacts(ctx, l.taskID)
handoff := compacting && finished
if sha != "" {
payload["commit"] = sha
+140
View File
@@ -0,0 +1,140 @@
package agents
import (
"context"
"errors"
"log/slog"
"g.fc.run/ai/byre/internal/api"
"g.fc.run/ai/byre/internal/git"
"g.fc.run/ai/byre/internal/store"
)
const allTasks = "*"
func (m *Manager) observeFacts(ev api.Event) {
switch ev.Kind {
case "task.created", "turn.ended", "snapshot.created", "review.started", "review.finished", "review.finding", "user.message", "board.posted":
if ev.TaskId != nil {
m.MarkFacts(*ev.TaskId)
}
case "task.merged", "task.merged_to_main":
if ev.ProjectId != nil {
m.MarkFacts("project:" + *ev.ProjectId)
}
case "user.updated":
m.MarkFacts(allTasks)
}
}
func (m *Manager) MarkFacts(key string) {
m.factsMu.Lock()
m.factsDirty[key] = true
m.factsMu.Unlock()
select {
case m.factsPoke <- struct{}{}:
default:
}
}
func (m *Manager) factsLoop() {
for {
select {
case <-m.watchStop:
return
case <-m.factsPoke:
}
m.factsMu.Lock()
dirty := m.factsDirty
m.factsDirty = map[string]bool{}
m.factsMu.Unlock()
err := m.settleFacts(context.Background(), dirty)
if err != nil {
slog.Error("settle merge facts", "err", err)
}
}
}
func (m *Manager) settleFacts(ctx context.Context, dirty map[string]bool) error {
tasks, err := m.store.ActiveTasks(ctx)
if err != nil {
return err
}
projects := map[string]api.Project{}
for _, t := range tasks {
if !dirty[allTasks] && !dirty[t.Id] && !dirty["project:"+t.ProjectId] {
continue
}
p, ok := projects[t.ProjectId]
if !ok {
p, err = m.store.GetProject(ctx, t.ProjectId)
if err != nil {
return err
}
projects[t.ProjectId] = p
}
err = m.settleTaskFacts(ctx, t, p)
if err != nil {
return err
}
}
return nil
}
func (m *Manager) refreshFacts(ctx context.Context, taskID string) {
t, err := m.store.GetTask(ctx, taskID)
if err == nil {
var p api.Project
p, err = m.store.GetProject(ctx, t.ProjectId)
if err == nil {
err = m.settleTaskFacts(ctx, t, p)
}
}
if err != nil {
slog.Error("refresh merge facts", "task", taskID, "err", err)
}
}
func (m *Manager) settleTaskFacts(ctx context.Context, t api.Task, p api.Project) error {
m.factsRun.Lock()
defer m.factsRun.Unlock()
g, err := m.gitFacts(ctx, t, p)
if err != nil {
slog.Error("compute merge facts", "task", t.Id, "err", err)
return nil
}
err = m.store.SettleMergeFacts(ctx, t.Id, g)
if errors.Is(err, store.ErrNotFound) {
return nil
}
return err
}
func (m *Manager) gitFacts(ctx context.Context, t api.Task, p api.Project) (store.GitFacts, error) {
g := store.GitFacts{}
if t.WorktreePath == "" {
return g, nil
}
tree, err := git.Snapshot(t.WorktreePath)
if err != nil {
return g, err
}
g.UnmergedCommits, err = git.Ahead(t.WorktreePath, p.DefaultBranch)
if err != nil {
return g, err
}
all, err := git.Compare(t.WorktreePath, p.DefaultBranch, "HEAD", tree, "")
if err != nil {
return g, err
}
g.UnmergedFiles = len(all.Files)
latest, err := m.store.LastReviewable(ctx, t.Id)
if errors.Is(err, store.ErrNotFound) {
return g, nil
}
if err != nil {
return g, err
}
g.UnreviewedChanges = latest.TreeSha != tree
return g, nil
}