From 890fb6b69184072f54fe577515deeb8cbfccbb51 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Wed, 21 Oct 2026 11:48:21 -0700 Subject: [PATCH] Open merge dialog instantly from pushed merge facts --- api/openapi.yaml | 67 +- internal/agents/lead.go | 1 + internal/agents/manager.go | 10 + internal/agents/mergefacts.go | 140 ++++ internal/api/api.gen.go | 755 +++++++++--------- internal/git/git.go | 43 +- internal/server/server.go | 39 +- internal/store/mergefacts.go | 81 ++ .../migrations/091-add-task-merge-facts.sql | 17 + internal/store/store.go | 21 +- internal/terminal/terminal.go | 23 +- mac/Sources/Byre/Chat.swift | 38 +- mac/Sources/Byre/Dialogs.swift | 8 +- mac/Sources/Byre/MergeDialog.swift | 167 ++-- mac/Sources/Byre/Store.swift | 4 + 15 files changed, 736 insertions(+), 678 deletions(-) create mode 100644 internal/agents/mergefacts.go create mode 100644 internal/store/mergefacts.go create mode 100644 internal/store/migrations/091-add-task-merge-facts.sql diff --git a/api/openapi.yaml b/api/openapi.yaml index 52668ab..666ee1e 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -466,40 +466,6 @@ paths: schema: { $ref: "#/components/schemas/WorkflowRun" } "404": { $ref: "#/components/responses/Error" } - /api/tasks/{taskId}/unmerged: - parameters: - - { name: taskId, in: path, required: true, schema: { type: string } } - get: - operationId: getUnmerged - description: What the task's worktree holds that its project's default branch doesn't. - responses: - "200": - description: The unmerged changes. - content: - application/json: - schema: { $ref: "#/components/schemas/Unmerged" } - "404": { $ref: "#/components/responses/Error" } - "409": { $ref: "#/components/responses/Error" } - - /api/tasks/{taskId}/co-authors: - parameters: - - { name: taskId, in: path, required: true, schema: { type: string } } - get: - operationId: getCoAuthors - description: > - The humans other than the caller with a message in the task's transcript (sent its agent a message, or had - an inbox answer delivered under their name), in order of their first message. A merge by the caller gives each one with a git identity a - Co-authored-by trailer. - responses: - "200": - description: The co-authors. - content: - application/json: - schema: - type: array - items: { $ref: "#/components/schemas/Participant" } - "404": { $ref: "#/components/responses/Error" } - /api/tasks/{taskId}/merge: parameters: - { name: taskId, in: path, required: true, schema: { type: string } } @@ -1478,7 +1444,7 @@ components: Task: type: object - required: [id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, state, merging, discarding, merging_into_parent, merge_after_review, summary_seq, background] + required: [id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, state, merging, discarding, merging_into_parent, merge_after_review, summary_seq, background, merge_facts] properties: id: { type: string } project_id: { type: string } @@ -1516,6 +1482,7 @@ components: merging_into_parent: { type: boolean, description: "Whether a merge of this subtask into its parent was requested and the parent hasn't merged it yet." } merge_after_review: { type: boolean, description: "Whether a Merge with after_review is waiting for the running review of the latest snapshot to finish." } summary_seq: { type: integer, format: int64, description: "The seq of the task's latest turn.ended event with summary: true; 0 when it has none." } + merge_facts: { $ref: "#/components/schemas/MergeFacts" } Todo: type: object @@ -1542,15 +1509,31 @@ components: model: { type: string } discuss: { type: boolean, description: "Tell the agent the human wants to discuss the item in chat before anything is built." } - Unmerged: + MergeFacts: type: object - required: [commits, files, insertions, deletions, uncommitted] + description: > + What a Merge of the task would land and how far it's been reviewed. The store keeps it on the task and + recomputes it only on events that can change it (a turn ending, a snapshot, a review starting, finishing or + having a finding answered, a human's message, the project's default branch moving, a user's git identity + changing); a task.merge_facts event, whose payload is the new MergeFacts, carries each change, and none is + sent when nothing changed. + required: [unmerged_commits, unmerged_files, unreviewed_changes, review_snapshot, review_count, review_running, review_failed, review_findings, participants] properties: - commits: { type: integer, description: Commits on the task branch that aren't on the default branch. } - files: { type: integer, description: Files that differ from the merge base, uncommitted changes included. } - insertions: { type: integer } - deletions: { type: integer } - uncommitted: { type: integer, description: Paths with uncommitted changes in the worktree. } + unmerged_commits: { type: integer, description: Commits on the task branch that aren't on the default branch. } + unmerged_files: { type: integer, description: "Files that differ from the merge base, uncommitted changes included." } + unreviewed_changes: { type: boolean, description: Whether the worktree differs from the latest reviewable snapshot. } + review_snapshot: { type: integer, description: "The n of the latest snapshot that isn't a cut point (answer, fork, ask_human); 0 when there is none." } + review_count: { type: integer, description: The number of reviews of review_snapshot. } + review_running: { type: boolean, description: Whether a review of review_snapshot is running. } + review_failed: { type: boolean, description: Whether a review of review_snapshot failed. } + review_findings: { type: integer, description: "Findings of review_snapshot's finished reviews that aren't ignored." } + participants: + type: array + description: > + The humans with a message in the task's transcript (sent its agent a message, or had an inbox answer + delivered under their name), in order of their first message. A merge gives each one other than the + merger with a git identity a Co-authored-by trailer. + items: { $ref: "#/components/schemas/Participant" } Participant: type: object diff --git a/internal/agents/lead.go b/internal/agents/lead.go index a4f2a38..434a218 100644 --- a/internal/agents/lead.go +++ b/internal/agents/lead.go @@ -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) diff --git a/internal/agents/manager.go b/internal/agents/manager.go index ade4759..3d35c10 100644 --- a/internal/agents/manager.go +++ b/internal/agents/manager.go @@ -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 diff --git a/internal/agents/mergefacts.go b/internal/agents/mergefacts.go new file mode 100644 index 0000000..a84fa93 --- /dev/null +++ b/internal/agents/mergefacts.go @@ -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 +} diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index 25a61c3..4d5591d 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -1023,6 +1023,36 @@ type LeadNote struct { UpdatedAt time.Time `json:"updated_at"` } +// MergeFacts What a Merge of the task would land and how far it's been reviewed. The store keeps it on the task and recomputes it only on events that can change it (a turn ending, a snapshot, a review starting, finishing or having a finding answered, a human's message, the project's default branch moving, a user's git identity changing); a task.merge_facts event, whose payload is the new MergeFacts, carries each change, and none is sent when nothing changed. +type MergeFacts struct { + // Participants The humans with a message in the task's transcript (sent its agent a message, or had an inbox answer delivered under their name), in order of their first message. A merge gives each one other than the merger with a git identity a Co-authored-by trailer. + Participants []Participant `json:"participants"` + + // ReviewCount The number of reviews of review_snapshot. + ReviewCount int `json:"review_count"` + + // ReviewFailed Whether a review of review_snapshot failed. + ReviewFailed bool `json:"review_failed"` + + // ReviewFindings Findings of review_snapshot's finished reviews that aren't ignored. + ReviewFindings int `json:"review_findings"` + + // ReviewRunning Whether a review of review_snapshot is running. + ReviewRunning bool `json:"review_running"` + + // ReviewSnapshot The n of the latest snapshot that isn't a cut point (answer, fork, ask_human); 0 when there is none. + ReviewSnapshot int `json:"review_snapshot"` + + // UnmergedCommits Commits on the task branch that aren't on the default branch. + UnmergedCommits int `json:"unmerged_commits"` + + // UnmergedFiles Files that differ from the merge base, uncommitted changes included. + UnmergedFiles int `json:"unmerged_files"` + + // UnreviewedChanges Whether the worktree differs from the latest reviewable snapshot. + UnreviewedChanges bool `json:"unreviewed_changes"` +} + // MessageCreate defines model for MessageCreate. type MessageCreate struct { // Images Blob ids from uploadBlob, sent to the agent as images alongside the text. @@ -1372,6 +1402,9 @@ type Task struct { // MergeCommit merged: the squash commit on the default branch; empty when there was nothing to merge. MergeCommit *string `json:"merge_commit,omitempty"` + // MergeFacts What a Merge of the task would land and how far it's been reviewed. The store keeps it on the task and recomputes it only on events that can change it (a turn ending, a snapshot, a review starting, finishing or having a finding answered, a human's message, the project's default branch moving, a user's git identity changing); a task.merge_facts event, whose payload is the new MergeFacts, carries each change, and none is sent when nothing changed. + MergeFacts MergeFacts `json:"merge_facts"` + // MergeQueuedAt When Merge was requested while another task in the project was merging; absent once the merge starts or when none is held. MergeQueuedAt *time.Time `json:"merge_queued_at,omitempty"` @@ -1525,20 +1558,6 @@ type TurnRef struct { Vendor string `json:"vendor"` } -// Unmerged defines model for Unmerged. -type Unmerged struct { - // Commits Commits on the task branch that aren't on the default branch. - Commits int `json:"commits"` - Deletions int `json:"deletions"` - - // Files Files that differ from the merge base - Files int `json:"files"` - Insertions int `json:"insertions"` - - // Uncommitted Paths with uncommitted changes in the worktree. - Uncommitted int `json:"uncommitted"` -} - // Usage defines model for Usage. type Usage struct { // Rejected Why the vendor is refusing work, such as a usage cap or depleted credits; absent while it isn't. A window at its limit also counts as rejected. @@ -2020,9 +2039,6 @@ type ServerInterface interface { // (GET /api/tasks/{taskId}/changes) GetTaskChanges(w http.ResponseWriter, r *http.Request, taskId string, params GetTaskChangesParams) - // (GET /api/tasks/{taskId}/co-authors) - GetCoAuthors(w http.ResponseWriter, r *http.Request, taskId string) - // (GET /api/tasks/{taskId}/diff) GetTaskDiff(w http.ResponseWriter, r *http.Request, taskId string, params GetTaskDiffParams) @@ -2089,9 +2105,6 @@ type ServerInterface interface { // (GET /api/tasks/{taskId}/terminals/{terminalId}) GetTaskTerminal(w http.ResponseWriter, r *http.Request, taskId string, terminalId string, params GetTaskTerminalParams) - // (GET /api/tasks/{taskId}/unmerged) - GetUnmerged(w http.ResponseWriter, r *http.Request, taskId string) - // (GET /api/tasks/{taskId}/workflow) GetTaskWorkflow(w http.ResponseWriter, r *http.Request, taskId string) @@ -3332,32 +3345,6 @@ func (siw *ServerInterfaceWrapper) GetTaskChanges(w http.ResponseWriter, r *http handler.ServeHTTP(w, r) } -// GetCoAuthors operation middleware -func (siw *ServerInterfaceWrapper) GetCoAuthors(w http.ResponseWriter, r *http.Request) { - - var err error - _ = err - - // ------------- Path parameter "taskId" ------------- - var taskId string - - err = runtime.BindStyledParameterWithOptions("simple", "taskId", r.PathValue("taskId"), &taskId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "taskId", Err: err}) - return - } - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetCoAuthors(w, r, taskId) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - // GetTaskDiff operation middleware func (siw *ServerInterfaceWrapper) GetTaskDiff(w http.ResponseWriter, r *http.Request) { @@ -4127,32 +4114,6 @@ func (siw *ServerInterfaceWrapper) GetTaskTerminal(w http.ResponseWriter, r *htt handler.ServeHTTP(w, r) } -// GetUnmerged operation middleware -func (siw *ServerInterfaceWrapper) GetUnmerged(w http.ResponseWriter, r *http.Request) { - - var err error - _ = err - - // ------------- Path parameter "taskId" ------------- - var taskId string - - err = runtime.BindStyledParameterWithOptions("simple", "taskId", r.PathValue("taskId"), &taskId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "taskId", Err: err}) - return - } - - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetUnmerged(w, r, taskId) - })) - - for _, middleware := range siw.HandlerMiddlewares { - handler = middleware(handler) - } - - handler.ServeHTTP(w, r) -} - // GetTaskWorkflow operation middleware func (siw *ServerInterfaceWrapper) GetTaskWorkflow(w http.ResponseWriter, r *http.Request) { @@ -4464,8 +4425,6 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/workflow", wrapper.GetTaskWorkflow) m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/tasks/{taskId}/summary-read", wrapper.MarkSummaryRead) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/workflow/run", wrapper.GetWorkflowRun) - m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/unmerged", wrapper.GetUnmerged) - m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/co-authors", wrapper.GetCoAuthors) m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/tasks/{taskId}/merge", wrapper.MergeTask) m.HandleFunc(http.MethodDelete+" "+options.BaseURL+"/api/tasks/{taskId}/merge-after-review", wrapper.CancelMergeAfterReview) m.HandleFunc(http.MethodDelete+" "+options.BaseURL+"/api/tasks/{taskId}/ready", wrapper.DismissReady) @@ -4525,328 +4484,332 @@ 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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", - "UKbQ9vPVIBqRuAmDYcBA0wFr8tWYLD58+F8DAA==", + "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", + "DHHEmnw1Cu0PH/7XAA==", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/git/git.go b/internal/git/git.go index 94014a7..0209c0a 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -109,14 +109,6 @@ func RemoveWorktree(repo, path, branch string) error { return err } -type Unmerged struct { - Commits int - Files int - Insertions int - Deletions int - Uncommitted int -} - type ConflictError struct { Files []string } @@ -125,41 +117,12 @@ func (e *ConflictError) Error() string { return "merge conflicts in " + strings.Join(e.Files, ", ") } -func UnmergedChanges(worktree, target string) (Unmerged, error) { - u := Unmerged{} - +func Ahead(worktree, target string) (int, error) { count, err := run(worktree, "rev-list", "--count", target+"..HEAD") if err != nil { - return u, err + return 0, err } - u.Commits, err = strconv.Atoi(count) - if err != nil { - return u, err - } - - status, err := run(worktree, "status", "--porcelain", "--untracked-files=all") - if err != nil { - return u, err - } - if status != "" { - u.Uncommitted = len(strings.Split(status, "\n")) - } - - tree, err := Snapshot(worktree) - if err != nil { - return u, err - } - changes, err := Compare(worktree, target, "HEAD", tree, "") - if err != nil { - return u, err - } - u.Files = len(changes.Files) - for _, file := range changes.Files { - u.Insertions += file.Additions - u.Deletions += file.Deletions - } - - return u, nil + return strconv.Atoi(count) } func Behind(worktree, target string) (int, error) { diff --git a/internal/server/server.go b/internal/server/server.go index c864db9..6c1d6f2 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -67,7 +67,9 @@ func New(dataDir string, portFirst int, ts *tsnet.Server, token string) (*Server startedAt: time.Now().UTC(), users: map[string]string{}, } - s.terminals, err = terminal.New(filepath.Join(dataDir, "zsh"), s.terminalsChanged) + s.terminals, err = terminal.New(filepath.Join(dataDir, "zsh"), s.terminalsChanged, func(taskID string) { + s.agents.MarkFacts(taskID) + }) if err != nil { return nil, err } @@ -490,41 +492,6 @@ func (s *Server) GetTask(w http.ResponseWriter, r *http.Request, taskID string) writeJSON(w, http.StatusOK, t) } -func (s *Server) GetUnmerged(w http.ResponseWriter, r *http.Request, taskID string) { - t, p, ok := s.activeTaskProject(w, r, taskID) - if !ok { - return - } - - u, err := git.UnmergedChanges(t.WorktreePath, p.DefaultBranch) - if err != nil { - writeError(w, http.StatusInternalServerError, err) - return - } - writeJSON(w, http.StatusOK, api.Unmerged{ - Commits: u.Commits, - Files: u.Files, - Insertions: u.Insertions, - Deletions: u.Deletions, - Uncommitted: u.Uncommitted, - }) -} - -func (s *Server) GetCoAuthors(w http.ResponseWriter, r *http.Request, taskID string) { - _, err := s.store.GetTask(r.Context(), taskID) - if err != nil { - writeStoreError(w, err) - return - } - - coAuthors, err := s.agents.CoAuthors(r.Context(), taskID, actor(r)) - if err != nil { - writeError(w, http.StatusInternalServerError, err) - return - } - writeJSON(w, http.StatusOK, coAuthors) -} - func (s *Server) MergeTask(w http.ResponseWriter, r *http.Request, taskID string) { req := api.MergeTaskJSONRequestBody{} if !readJSON(w, r, &req) { diff --git a/internal/store/mergefacts.go b/internal/store/mergefacts.go new file mode 100644 index 0000000..cb534f0 --- /dev/null +++ b/internal/store/mergefacts.go @@ -0,0 +1,81 @@ +package store + +import ( + "context" + "database/sql" + "errors" + "reflect" + + "g.fc.run/ai/byre/internal/api" +) + +const mergeFactsColumns = `unmerged_commits, unmerged_files, unreviewed_changes, review_snapshot, review_count, review_running, review_failed, review_findings, + (SELECT json_group_array(json_object('login', login, 'display_name', display_name, 'git_name', git_name, 'git_email', git_email)) + FROM (SELECT * FROM task_participants WHERE task_participants.task_id = tasks.id ORDER BY position))` + +type GitFacts struct { + UnmergedCommits int + UnmergedFiles int + UnreviewedChanges bool +} + +func (s *Store) SettleMergeFacts(ctx context.Context, taskID string, g GitFacts) error { + return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { + t, err := scanTask(tx.QueryRowContext(ctx, `SELECT `+taskColumns+` FROM tasks WHERE id = ?`, taskID)) + if errors.Is(err, sql.ErrNoRows) { + return ErrNotFound + } + if err != nil { + return err + } + if t.Status != api.TaskStatusActive { + return nil + } + + f := api.MergeFacts{UnmergedCommits: g.UnmergedCommits, UnmergedFiles: g.UnmergedFiles, UnreviewedChanges: g.UnreviewedChanges} + err = tx.QueryRowContext(ctx, `SELECT coalesce(max(n), 0) FROM snapshots WHERE task_id = ? AND trigger NOT IN (?, ?, ?)`, + taskID, string(api.SnapshotTriggerAnswer), string(api.SnapshotTriggerFork), string(api.SnapshotTriggerAskHuman)).Scan(&f.ReviewSnapshot) + if err != nil { + return err + } + err = tx.QueryRowContext(ctx, ` + SELECT count(*), coalesce(max(state = ?), 0), coalesce(max(state = ?), 0), + coalesce(sum(CASE WHEN state = ? THEN ( + SELECT count(*) FROM json_each(nullif(reviews.result, ''), '$.findings') f + WHERE coalesce(json_extract(reviews.finding_states, '$."' || f.key || '"'), '') != ? + ) END), 0) + FROM reviews WHERE task_id = ? AND snapshot_n = ? + `, string(api.ReviewStateRunning), string(api.ReviewStateFailed), string(api.ReviewStateDone), string(api.FindingIgnored), taskID, f.ReviewSnapshot). + Scan(&f.ReviewCount, &f.ReviewRunning, &f.ReviewFailed, &f.ReviewFindings) + if err != nil { + return err + } + f.Participants, err = participants(ctx, tx, taskID) + if err != nil { + return err + } + + if reflect.DeepEqual(f, t.MergeFacts) { + return nil + } + _, err = tx.ExecContext(ctx, ` + UPDATE tasks SET unmerged_commits = ?, unmerged_files = ?, unreviewed_changes = ?, review_snapshot = ?, review_count = ?, review_running = ?, review_failed = ?, review_findings = ? + WHERE id = ? + `, f.UnmergedCommits, f.UnmergedFiles, f.UnreviewedChanges, f.ReviewSnapshot, f.ReviewCount, f.ReviewRunning, f.ReviewFailed, f.ReviewFindings, taskID) + if err != nil { + return err + } + _, err = tx.ExecContext(ctx, `DELETE FROM task_participants WHERE task_id = ?`, taskID) + if err != nil { + return err + } + for i, p := range f.Participants { + _, err = tx.ExecContext(ctx, `INSERT INTO task_participants (task_id, position, login, display_name, git_name, git_email) VALUES (?, ?, ?, ?, ?, ?)`, + taskID, i, p.Login, p.DisplayName, p.GitName, p.GitEmail) + if err != nil { + return err + } + } + return emit(api.Event{Kind: "task.merge_facts", ProjectId: &t.ProjectId, TaskId: &taskID, Actor: "byre", Payload: ToPayload(f)}) + }) +} diff --git a/internal/store/migrations/091-add-task-merge-facts.sql b/internal/store/migrations/091-add-task-merge-facts.sql new file mode 100644 index 0000000..6ff0068 --- /dev/null +++ b/internal/store/migrations/091-add-task-merge-facts.sql @@ -0,0 +1,17 @@ +ALTER TABLE tasks ADD COLUMN unmerged_commits INTEGER NOT NULL DEFAULT 0; +ALTER TABLE tasks ADD COLUMN unmerged_files INTEGER NOT NULL DEFAULT 0; +ALTER TABLE tasks ADD COLUMN unreviewed_changes INTEGER NOT NULL DEFAULT 0; +ALTER TABLE tasks ADD COLUMN review_snapshot INTEGER NOT NULL DEFAULT 0; +ALTER TABLE tasks ADD COLUMN review_count INTEGER NOT NULL DEFAULT 0; +ALTER TABLE tasks ADD COLUMN review_running INTEGER NOT NULL DEFAULT 0; +ALTER TABLE tasks ADD COLUMN review_failed INTEGER NOT NULL DEFAULT 0; +ALTER TABLE tasks ADD COLUMN review_findings INTEGER NOT NULL DEFAULT 0; +CREATE TABLE task_participants ( + task_id TEXT NOT NULL REFERENCES tasks (id), + position INTEGER NOT NULL, + login TEXT NOT NULL, + display_name TEXT NOT NULL, + git_name TEXT NOT NULL, + git_email TEXT NOT NULL, + PRIMARY KEY (task_id, position) +); diff --git a/internal/store/store.go b/internal/store/store.go index 3c3c259..6d9c059 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -558,6 +558,10 @@ func (s *Store) queryTasks(ctx context.Context, query string, args ...any) ([]ap return tasks, rows.Err() } +func (s *Store) ActiveTasks(ctx context.Context) ([]api.Task, error) { + return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE status = ? ORDER BY created_at`, string(api.TaskStatusActive)) +} + func (s *Store) ActiveSubtasks(ctx context.Context, parentID string) ([]api.Task, error) { return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE parent_task_id = ? AND status = ? ORDER BY created_at`, parentID, string(api.TaskStatusActive)) } @@ -939,7 +943,11 @@ func closeRun(ctx context.Context, tx *sql.Tx, emit emitFunc, stageID, actor str } func (s *Store) Participants(ctx context.Context, taskID string) ([]api.Participant, error) { - rows, err := s.db.QueryContext(ctx, ` + return participants(ctx, s.db, taskID) +} + +func participants(ctx context.Context, q querier, taskID string) ([]api.Participant, error) { + rows, err := q.QueryContext(ctx, ` SELECT u.login, u.display_name, u.git_name, u.git_email FROM events e JOIN users u ON u.login = e.actor WHERE e.task_id = ? AND (e.kind IN ('user.message', 'board.posted') OR (e.kind = 'task.created' AND json_extract(e.payload, '$.forked_from') IS NOT NULL)) @@ -1702,7 +1710,7 @@ func (s *Store) ClearReviewRequests(ctx context.Context) error { const projectColumns = `id, name, repo_path, default_branch, remote, created_by, created_at, review_workflow, audit_workflow` -const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at, workflow_name, workflow_stage, parent_task_id, parent_merge_by, merge_after_review_by != '', state, merging_by != '' OR finalizing != '', discard_state != '' AND status = 'active', (SELECT coalesce(max(seq), 0) FROM events WHERE events.task_id = tasks.id AND kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1), audit, ` + backgroundExpr +const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at, workflow_name, workflow_stage, parent_task_id, parent_merge_by, merge_after_review_by != '', state, merging_by != '' OR finalizing != '', discard_state != '' AND status = 'active', (SELECT coalesce(max(seq), 0) FROM events WHERE events.task_id = tasks.id AND kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1), audit, ` + backgroundExpr + `, ` + mergeFactsColumns // Keep the child lookup separate from the task itself so SQLite can use tasks_parent. const backgroundExpr = `(audit != '' OR EXISTS (SELECT 1 FROM tasks r WHERE r.id = tasks.parent_task_id AND tasks.workflow_stage != '' AND r.audit != '')) @@ -1742,7 +1750,14 @@ func scanTask(row scanner) (api.Task, error) { parentMergeBy := "" taskState := "" audit := "" - err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &t.Color, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt, &workflowName, &workflowStage, &parentTaskID, &parentMergeBy, &t.MergeAfterReview, &taskState, &t.Merging, &t.Discarding, &t.SummarySeq, &audit, &t.Background) + participants := "" + f := &t.MergeFacts + err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &t.Color, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt, &workflowName, &workflowStage, &parentTaskID, &parentMergeBy, &t.MergeAfterReview, &taskState, &t.Merging, &t.Discarding, &t.SummarySeq, &audit, &t.Background, + &f.UnmergedCommits, &f.UnmergedFiles, &f.UnreviewedChanges, &f.ReviewSnapshot, &f.ReviewCount, &f.ReviewRunning, &f.ReviewFailed, &f.ReviewFindings, &participants) + if err != nil { + return t, err + } + err = json.Unmarshal([]byte(participants), &f.Participants) if err != nil { return t, err } diff --git a/internal/terminal/terminal.go b/internal/terminal/terminal.go index 359039e..4c3b815 100644 --- a/internal/terminal/terminal.go +++ b/internal/terminal/terminal.go @@ -57,19 +57,21 @@ fi ` type Terminals struct { - zdotdir string - changed func(taskID string) + zdotdir string + changed func(taskID string) + prompted func(taskID string) mu sync.Mutex byTask map[string][]*Terminal } type Terminal struct { - ID string - Name string - ptmx *os.File - cmd *exec.Cmd - Prompts bool + ID string + Name string + ptmx *os.File + cmd *exec.Cmd + Prompts bool + prompted func() mu sync.Mutex backlog []byte @@ -89,7 +91,7 @@ type wake struct { out []byte } -func New(zdotdir string, changed func(taskID string)) (*Terminals, error) { +func New(zdotdir string, changed, prompted func(taskID string)) (*Terminals, error) { err := os.MkdirAll(zdotdir, 0o755) if err != nil { return nil, err @@ -98,7 +100,7 @@ func New(zdotdir string, changed func(taskID string)) (*Terminals, error) { if err != nil { return nil, err } - return &Terminals{zdotdir: zdotdir, changed: changed, byTask: map[string][]*Terminal{}}, nil + return &Terminals{zdotdir: zdotdir, changed: changed, prompted: prompted, byTask: map[string][]*Terminal{}}, nil } func (ts *Terminals) List(taskID string) []*Terminal { @@ -159,7 +161,7 @@ func (ts *Terminals) start(t api.Task, name string, size api.TerminalSize) (*Ter return nil, fmt.Errorf("start %s: %w", shell, err) } - term := &Terminal{ID: ids.New(ids.Terminal), Name: name, ptmx: ptmx, cmd: cmd, Prompts: prompts, clients: map[chan []byte]bool{}, started: time.Now()} + term := &Terminal{ID: ids.New(ids.Terminal), Name: name, ptmx: ptmx, cmd: cmd, Prompts: prompts, prompted: func() { ts.prompted(t.Id) }, clients: map[chan []byte]bool{}, started: time.Now()} ts.byTask[t.Id] = append(ts.byTask[t.Id], term) slog.Info("terminal started", "task", t.Id, "terminal", term.ID, "name", name, "shell", shell, "pid", cmd.Process.Pid) go func() { @@ -338,6 +340,7 @@ func (term *Terminal) trackLine(b []byte) { switch { case string(prefix) == promptMark: term.atPrompt, term.line = true, "" + go term.prompted() case body == "E": term.atPrompt, term.line = false, "" case strings.HasPrefix(body, "L"): diff --git a/mac/Sources/Byre/Chat.swift b/mac/Sources/Byre/Chat.swift index 8654901..93929ec 100644 --- a/mac/Sources/Byre/Chat.swift +++ b/mac/Sources/Byre/Chat.swift @@ -36,11 +36,9 @@ final class ChatModel { private(set) var scrollRequest: (seq: Int, id: Int?, anchor: UnitPoint)? private(set) var fetching = false private(set) var older: [ByreEvent]? - private(set) var hasUnmerged = false @ObservationIgnored var stuck = true @ObservationIgnored var prepended = false @ObservationIgnored var released: Int? - @ObservationIgnored private var unmergedTimer: Task? @ObservationIgnored private var offs: [() -> Void] = [] @ObservationIgnored private var focusStart = true @ObservationIgnored private var visibleSummaries: Set = [] @@ -81,6 +79,13 @@ final class ChatModel { return nil } + var hasUnmerged: Bool { + guard let f = taskId.flatMap(store.findTask)?.mergeFacts else { + return false + } + return f.unmergedCommits > 0 || f.unmergedFiles > 0 + } + private var feed: Feed? { switch kind { case .lead: store.leadFeed @@ -161,9 +166,6 @@ final class ChatModel { guard let feed else { return } - Task { - await loadUnmerged() - } appended(feed.events) offs.append(feed.appended.add { [weak self] events in self?.appended(events) @@ -194,39 +196,15 @@ final class ChatModel { NotificationCenter.default.removeObserver(keyObserver) } keyObserver = nil - unmergedTimer?.cancel() - unmergedTimer = nil builder.stop() store.drafts?.flush() } private func appended(_ all: [ByreEvent]) { - let events = all.filter(shows) - if events.contains(where: { $0.kind == "agent.tool_result" || $0.kind == "turn.ended" }) { - unmergedTimer?.cancel() - unmergedTimer = Task { [weak self] in - try? await Task.sleep(for: .milliseconds(1500)) - if !Task.isCancelled { - await self?.loadUnmerged() - } - } - } - _ = builder.append(events) + _ = builder.append(all.filter(shows)) jump() } - private func loadUnmerged() async { - guard let taskId else { - return - } - do { - let u = try await store.api.client.getUnmerged(path: .init(taskId: taskId)).ok.body.json - hasUnmerged = u.commits > 0 || u.files > 0 - } catch { - log.error("load unmerged changes: \(errorText(error), privacy: .public)") - } - } - func takeFocus() -> Bool { let asked = taskId != nil && store.composerFocus == taskId if asked { diff --git a/mac/Sources/Byre/Dialogs.swift b/mac/Sources/Byre/Dialogs.swift index b3b5a44..e9d2acf 100644 --- a/mac/Sources/Byre/Dialogs.swift +++ b/mac/Sources/Byre/Dialogs.swift @@ -23,7 +23,7 @@ struct ModalButton { struct OverlayModal { let content: AnyView - let buttons: [ModalButton] + let buttons: @MainActor () -> [ModalButton] let anchor: CGRect? let inPane: Bool let canSubmit: @MainActor () -> Bool @@ -79,7 +79,7 @@ final class Overlays { alerts.append(OverlayAlert(content: AnyView(content))) } - func showModal(_ content: some View, _ buttons: [ModalButton], anchor: CGRect? = nil, inPane: Bool = false, canSubmit: @escaping @MainActor () -> Bool = { true }) async -> String { + func showModal(_ content: some View, _ buttons: @autoclosure @escaping @MainActor () -> [ModalButton], anchor: CGRect? = nil, inPane: Bool = false, canSubmit: @escaping @MainActor () -> Bool = { true }) async -> String { modal?.done("cancel") menu = nil return await withCheckedContinuation { cont in @@ -183,7 +183,7 @@ final class Overlays { } if let modal { if enter && !marked { - if modal.canSubmit(), let last = modal.buttons.last { + if modal.canSubmit(), let last = modal.buttons().last { answerModal(last.value) } return true @@ -417,7 +417,7 @@ private struct ModalBox: View { .fixedSize(horizontal: false, vertical: true) HStack(spacing: 8) { Spacer() - ForEach(Array(modal.buttons.enumerated()), id: \.offset) { _, b in + ForEach(Array(modal.buttons().enumerated()), id: \.offset) { _, b in Button(b.label) { answer(b.value) } diff --git a/mac/Sources/Byre/MergeDialog.swift b/mac/Sources/Byre/MergeDialog.swift index 2595aa1..be2c412 100644 --- a/mac/Sources/Byre/MergeDialog.swift +++ b/mac/Sources/Byre/MergeDialog.swift @@ -2,6 +2,7 @@ import ByreAPI import SwiftUI typealias Participant = Components.Schemas.Participant +typealias MergeFacts = Components.Schemas.MergeFacts @MainActor private var mergeDrafts: [String: String] = [:] @@ -23,22 +24,17 @@ final class MergeModel { let target: String let keepOpen: Bool let parentTitle: String? - let coAuthors: [Participant] let original: String var title: String var editing = false var toParent: Bool - var reviewItems: [MergeWarning] = [] - @ObservationIgnored var running = false @ObservationIgnored var leave: Leave = { _ in } - @ObservationIgnored private var loads = 0 - init(taskId: String, target: String, keepOpen: Bool, parentTitle: String?, coAuthors: [Participant], title: String) { + init(taskId: String, target: String, keepOpen: Bool, parentTitle: String?, title: String) { self.taskId = taskId self.target = target self.keepOpen = keepOpen self.parentTitle = parentTitle - self.coAuthors = coAuthors original = title self.title = title toParent = parentTitle != nil @@ -51,26 +47,6 @@ final class MergeModel { var sentTitle: String { editing ? title : original } - - func load(_ store: Store) async { - loads += 1 - let started = loads - var items: [MergeWarning] - var isRunning = false - do { - (items, isRunning) = try await reviewWarnings(taskId, store, leave) - } catch { - items = [MergeWarning(text: "Review status unavailable", detail: errorText(error))] - } - if started == loads { - reviewItems = items - running = isRunning - } - } - - func stop() { - loads += 1 - } } @MainActor @@ -91,25 +67,14 @@ func confirmMerge(_ taskId: String, _ store: Store, _ overlays: Overlays, keepOp return } let target = store.projects.first { $0.id == task.projectId }?.defaultBranch ?? "the default branch" - let parent = task.workflowStage == nil ? task.parentTaskId.flatMap { store.findTask($0) } : nil - let coAuthors: [Participant] - do { - let client = store.api.client - async let unmerged = client.getUnmerged(path: .init(taskId: taskId)).ok.body.json - async let people = client.getCoAuthors(path: .init(taskId: taskId)).ok.body.json - let (u, p) = try await (unmerged, people) - if u.commits == 0 && u.files == 0 { - overlays.showAlert("Nothing to merge: no commits or uncommitted files differ from \(target)") - return - } - coAuthors = p - } catch { - overlays.showAlert(errorText(error)) + if task.mergeFacts.unmergedCommits == 0 && task.mergeFacts.unmergedFiles == 0 { + overlays.showAlert("Nothing to merge: no commits or uncommitted files differ from \(target)") return } + let parent = task.workflowStage == nil ? task.parentTaskId.flatMap { store.findTask($0) } : nil let title = mergeDrafts[taskId] ?? task.mergeTitle mergeDrafts[taskId] = nil - let model = MergeModel(taskId: taskId, target: target, keepOpen: keepOpen, parentTitle: !keepOpen && parent?.status == .active ? parent?.name : nil, coAuthors: coAuthors, title: title) + let model = MergeModel(taskId: taskId, target: target, keepOpen: keepOpen, parentTitle: !keepOpen && parent?.status == .active ? parent?.name : nil, title: title) model.leave = { [weak model] go in if let model { mergeDrafts[taskId] = model.sentTitle @@ -117,92 +82,58 @@ func confirmMerge(_ taskId: String, _ store: Store, _ overlays: Overlays, keepOp overlays.answerModal("cancel") go() } - let off = store.events.add { ev in - if ev.taskId == taskId && (ev.kind == "snapshot.created" || isReviewEvent(ev)) { - Task { - await model.load(store) - } - } - } - defer { - model.stop() - off() - } - await model.load(store) let content = MergeContent(model: model) - let merge = { (afterReview: Bool) in - let body: Operations.MergeTask.Input.Body.JsonPayload = model.toParent - ? .init(target: .parent, afterReview: afterReview ? true : nil) - : .init(mergeTitle: model.sentTitle, afterReview: afterReview ? true : nil, keepOpen: keepOpen ? true : nil) - do { - _ = try await store.api.client.mergeTask(path: .init(taskId: taskId), body: .json(body)).accepted - } catch { - overlays.showAlert(errorText(error)) - } - } - if keepOpen { - if await overlays.showModal(content, [ModalButton(label: "Cancel", value: "cancel"), ModalButton(label: "Merge & continue", value: "ok", primary: true)], inPane: true, canSubmit: { model.valid }) == "ok" { - await merge(false) - } + let choice = await overlays.showModal(content, keepOpen + ? [ModalButton(label: "Cancel", value: "cancel"), ModalButton(label: "Merge & continue", value: "now", primary: true)] + : store.findTask(taskId)?.mergeFacts.reviewRunning == true + ? [ + ModalButton(label: "Cancel", value: "cancel"), + ModalButton(label: "Merge & complete now", value: "now"), + ModalButton(label: "Merge & complete after review", value: "after", primary: true, help: "Merge once the running review finishes with no findings"), + ] + : [ModalButton(label: "Cancel", value: "cancel"), ModalButton(label: "Merge & complete", value: "now", primary: true)], + inPane: true, canSubmit: { model.valid }) + if choice != "now" && choice != "after" { return } - if !model.running { - if await overlays.showModal(content, [ModalButton(label: "Cancel", value: "cancel"), ModalButton(label: "Merge & complete", value: "ok", primary: true)], inPane: true, canSubmit: { model.valid }) == "ok" { - await merge(false) - } - return - } - let choice = await overlays.showModal(content, [ - ModalButton(label: "Cancel", value: "cancel"), - ModalButton(label: "Merge & complete now", value: "now"), - ModalButton(label: "Merge & complete after review", value: "after", primary: true, help: "Merge once the running review finishes with no findings"), - ], inPane: true, canSubmit: { model.valid }) - if choice == "now" || choice == "after" { - await merge(choice == "after") + let afterReview = choice == "after" + let body: Operations.MergeTask.Input.Body.JsonPayload = model.toParent + ? .init(target: .parent, afterReview: afterReview ? true : nil) + : .init(mergeTitle: model.sentTitle, afterReview: afterReview ? true : nil, keepOpen: keepOpen ? true : nil) + do { + _ = try await store.api.client.mergeTask(path: .init(taskId: taskId), body: .json(body)).accepted + } catch { + overlays.showAlert(errorText(error)) } } @MainActor -private func reviewWarnings(_ taskId: String, _ store: Store, _ leave: @escaping Leave) async throws -> ([MergeWarning], Bool) { - let client = store.api.client - async let snapsCall = client.listSnapshots(path: .init(taskId: taskId)).ok.body.json - async let reviewsCall = client.listReviews(path: .init(taskId: taskId)).ok.body.json - let (snaps, reviews) = try await (snapsCall, reviewsCall) - guard let latest = snaps.last(where: { ![.answer, .fork, .askHuman].contains($0.trigger) }) else { - return ([MergeWarning(text: "These changes haven't been reviewed")], false) +private func reviewWarnings(_ taskId: String, _ f: MergeFacts, _ store: Store, _ leave: @escaping Leave) -> [MergeWarning] { + if f.reviewSnapshot == 0 { + return [MergeWarning(text: "These changes haven't been reviewed")] } var items: [MergeWarning] = [] let open: @MainActor () -> Void = { leave { - store.jumpTo(taskId, .review(latest.n)) + store.jumpTo(taskId, .review(f.reviewSnapshot)) } } - let rs = reviews.filter { $0.snapshotN == latest.n } - for r in rs { - let found = unignored(r) - switch r.state { - case .running: - items.append(MergeWarning(text: "The \(r.stage) review of the latest snapshot is still running", open: open)) - case .failed: - items.append(MergeWarning(text: "The \(r.stage) review of the latest snapshot failed", open: open)) - default: - if !found.isEmpty { - items.append(MergeWarning(text: "The \(r.stage) review of the latest snapshot found \(severityCounts(found))", open: open)) - } - } - } - if rs.isEmpty { + if f.reviewCount == 0 { items.append(MergeWarning(text: "The latest snapshot hasn't been reviewed", open: open)) } - let changes = try await client.getTaskChanges(path: .init(taskId: taskId), query: .init(since: latest.commitSha)).ok.body.json - if !changes.files.isEmpty { + if f.reviewRunning { + items.append(MergeWarning(text: "The review of the latest snapshot is still running", open: open)) + } + if f.reviewFailed { + items.append(MergeWarning(text: "The review of the latest snapshot failed", open: open)) + } + if f.reviewFindings > 0 { + items.append(MergeWarning(text: "The review of the latest snapshot has \(f.reviewFindings) open finding\(f.reviewFindings == 1 ? "" : "s")", open: open)) + } + if f.unreviewedChanges { items.append(MergeWarning(text: "Changes since the last snapshot haven't been reviewed")) } - return (items, rs.contains { $0.state == .running }) -} - -private func unignored(_ r: Review) -> [JSONDict] { - r.state == .done ? findings(r).enumerated().filter { r.findingStates.additionalProperties[String($0.offset)] != .ignored }.map(\.element) : [] + return items } private func coAuthorsLabel(_ coAuthors: [Participant]) -> String { @@ -234,11 +165,13 @@ private struct MergeContent: View { @FocusState private var focused: Bool var body: some View { - let questions = store.inbox.filter { $0.taskId == model.taskId }.count - var warnings = model.reviewItems + let taskId = model.taskId + let leave = model.leave + let facts = store.findTask(taskId)?.mergeFacts + let coAuthors = facts?.participants.filter { $0.login != store.me?.login } ?? [] + let questions = store.inbox.filter { $0.taskId == taskId }.count + var warnings = facts.map { reviewWarnings(taskId, $0, store, leave) } ?? [] if questions > 0 { - let taskId = model.taskId - let leave = model.leave warnings.append(MergeWarning(text: questions == 1 ? "The agent's open question will be dismissed" : "The agent's \(questions) open questions will be dismissed", open: { leave { store.showQuestion(taskId) @@ -269,12 +202,12 @@ private struct MergeContent: View { .foregroundStyle(Palette.warn) .padding(.bottom, 16) } - if !model.toParent && !model.coAuthors.isEmpty { + if !model.toParent && !coAuthors.isEmpty { DetailsBox(boxed: false) { - Text(coAuthorsLabel(model.coAuthors)) + Text(coAuthorsLabel(coAuthors)) .foregroundStyle(Palette.muted) } content: { - ModalText(text: mergeTrailers(model.coAuthors)) + ModalText(text: mergeTrailers(coAuthors)) .padding(.top, 8) } .padding(.bottom, 16) diff --git a/mac/Sources/Byre/Store.swift b/mac/Sources/Byre/Store.swift index 449aba7..53755bb 100644 --- a/mac/Sources/Byre/Store.swift +++ b/mac/Sources/Byre/Store.swift @@ -702,6 +702,10 @@ final class Store { try await self.loadTasks(projectId) } } + case "task.merge_facts": + if let projectId = ev.projectId, let taskId = ev.taskId, let i = tasks[projectId]?.firstIndex(where: { $0.id == taskId }) { + tasks[projectId]![i].mergeFacts = try! decodePayload(ev.payload, as: MergeFacts.self) + } case "task.merge_stopped", "task.merge_after_review_dropped", "task.workflow_finished": notifyTask(self, ev) case "turn.ended":