Open merge dialog instantly from pushed merge facts
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user