Simplify codebase per audit of 0139564

This commit is contained in:
Ian Gulliver
2026-10-23 18:25:56 -07:00
parent 013956440d
commit 68ccff3f03
24 changed files with 122 additions and 190 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ func (m *Manager) MergeAfterReview(ctx context.Context, task api.Task, user api.
if err != nil {
return api.Task{}, err
}
if target == api.MergeTargetParent && (task.ParentTaskId == nil || task.WorkflowStage != nil) {
if target == api.MergeTargetParent && !isSubtask(task) {
return api.Task{}, fmt.Errorf("%w: task %s is not a subtask", ErrInvalid, task.Id)
}
if target == api.MergeTargetMain {
+6 -2
View File
@@ -221,9 +221,13 @@ func (m *Manager) agyBeforeModel(l *live) hookcmd.Func {
func (m *Manager) agyBeforeTool(l *live) hookcmd.Func {
return func(ctx context.Context, input json.RawMessage) (map[string]any, error) {
if !m.heldBeforeTool(ctx, l, "") || !m.midTurnWaiting(ctx, l) {
if !m.resumedFromPause(ctx, l, "") {
return map[string]any{"decision": "allow"}, nil
}
return map[string]any{"decision": "deny", "reason": pausedCallNotice}, nil
mt := m.deliverMidTurn(ctx, l)
if mt.text == "" {
return map[string]any{"decision": "allow"}, nil
}
return map[string]any{"decision": "deny", "reason": pausedCallNotice + "\n\n" + mt.text}, nil
}
}
+1 -1
View File
@@ -278,7 +278,7 @@ func (m *Manager) listClients() string {
now := time.Now()
b := &strings.Builder{}
for i, r := range rows {
fmt.Fprintf(b, "- %s (%s, %s, id %s): ", r.Host, r.Device, r.Login, r.ID)
fmt.Fprintf(b, "- %s (%s, %s, id %s): ", r.Host, r.Device, r.Login, r.Id)
if r.LastActionAt == nil {
b.WriteString("no action since byre started")
} else {
+1 -1
View File
@@ -210,7 +210,7 @@ func (m *Manager) steerMidTurn(ctx context.Context, l *live, s *codex.Session) {
func (m *Manager) codexAfterTool(l *live) hookcmd.Func {
return func(ctx context.Context, input json.RawMessage) (map[string]any, error) {
if m.holdWhilePaused(ctx, l, "") && ctx.Err() == nil {
if m.resumedFromPause(ctx, l, "") {
m.steerMidTurn(context.WithoutCancel(ctx), l, l.sess.(codexProcess).Session)
}
return map[string]any{}, nil
+1 -1
View File
@@ -109,7 +109,7 @@ func (m *Manager) taskGuidance(ctx context.Context, task api.Task) (string, erro
g += fmt.Sprintf("\n- This task was started from the item %q in %s at the repo root. When the work is done, remove that item from %s in this worktree, so the removal lands in the same merge as the work.",
*task.TodoItem, todo.File, todo.File)
}
if task.ParentTaskId != nil && task.WorkflowStage == nil {
if isSubtask(task) {
parent, err := m.store.GetTask(ctx, *task.ParentTaskId)
if err != nil {
return "", err
-17
View File
@@ -418,23 +418,6 @@ func (m *Manager) deliverMidTurn(ctx context.Context, l *live) midTurn {
return mt
}
func (m *Manager) midTurnWaiting(ctx context.Context, l *live) bool {
inputs, err := m.store.TaskInputs(ctx, l.taskID, l.member)
if err != nil {
slog.Error("list queued messages", "session", l.id, "err", err)
return false
}
if len(inputs) > 0 {
return true
}
pending, err := m.store.Undelivered(ctx, l.id)
if err != nil {
slog.Error("list undelivered responses", "session", l.id, "err", err)
return false
}
return len(pending) > 0
}
func (m *Manager) restoreMidTurn(ctx context.Context, l *live, mt midTurn) {
if len(mt.taskInputs) > 0 {
err := m.store.RestoreTaskInputs(ctx, mt.taskInputs)
+2 -2
View File
@@ -392,7 +392,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
return "", err
}
case "parent":
if task.ParentTaskId != nil {
if isSubtask(task) {
err = m.checkTaskNotStopped(ctx, *task.ParentTaskId)
if err != nil {
return "", err
@@ -878,7 +878,7 @@ func (m *Manager) listTasksText(ctx context.Context) (string, error) {
if t.ForkedFrom != nil {
fmt.Fprintf(b, "; %s", forkOrigin(t, *t.ForkedFrom))
}
if t.ParentTaskId != nil && t.WorkflowStage == nil {
if isSubtask(t) {
fmt.Fprintf(b, "; subtask of %s", *t.ParentTaskId)
if t.MergingIntoParent {
b.WriteString(", merging into it")
+2 -2
View File
@@ -166,13 +166,13 @@ func (m *Manager) holdWhilePaused(ctx context.Context, l *live, toolUseID string
}
}
func (m *Manager) heldBeforeTool(ctx context.Context, l *live, toolUseID string) bool {
func (m *Manager) resumedFromPause(ctx context.Context, l *live, toolUseID string) bool {
return m.holdWhilePaused(ctx, l, toolUseID) && ctx.Err() == nil
}
func (m *Manager) beforeTool(l *live) hookcmd.Func {
return func(ctx context.Context, input json.RawMessage) (map[string]any, error) {
if !m.heldBeforeTool(ctx, l, hookToolUseID(input)) {
if !m.resumedFromPause(ctx, l, hookToolUseID(input)) {
return map[string]any{}, nil
}
mt := m.deliverMidTurn(ctx, l)
+1 -1
View File
@@ -271,7 +271,7 @@ func (m *Manager) reviewPrompt(ctx context.Context, task api.Task, project api.P
}
func (m *Manager) reviewBase(ctx context.Context, task api.Task, project api.Project) (string, error) {
if task.ParentTaskId == nil || task.WorkflowStage != nil {
if !isSubtask(task) {
return project.DefaultBranch, nil
}
parent, err := m.store.GetTask(ctx, *task.ParentTaskId)
+2 -3
View File
@@ -7,7 +7,6 @@ import (
"time"
"g.fc.run/ai/byre/internal/api"
"g.fc.run/ai/byre/internal/clients"
)
const (
@@ -17,7 +16,7 @@ const (
func (m *Manager) signer(ctx context.Context, user api.User) func(payload []byte) ([]byte, error) {
return func(payload []byte) ([]byte, error) {
mine := []clients.Client{}
mine := []api.ConnectedClient{}
for _, c := range m.clients.RecentMacs() {
if c.Login == user.Login {
mine = append(mine, c)
@@ -33,7 +32,7 @@ func (m *Manager) signer(ctx context.Context, user api.User) func(payload []byte
ms := int(signTimeout.Milliseconds())
cctx, cancel := context.WithTimeout(ctx, signTimeout+replyGrace)
defer cancel()
reply, err := m.clients.Call(cctx, client.ID, api.ClientRequest{Op: api.Sign, Data: &payload, Ident: &ident, TimeoutMs: &ms})
reply, err := m.clients.Call(cctx, client.Id, api.ClientRequest{Op: api.Sign, Data: &payload, Ident: &ident, TimeoutMs: &ms})
if err != nil {
return nil, err
}
+6 -2
View File
@@ -274,8 +274,12 @@ func (m *Manager) messageTask(ctx context.Context, task api.Task, user api.User,
return err
}
func isSubtask(task api.Task) bool {
return task.ParentTaskId != nil && task.WorkflowStage == nil
}
func (m *Manager) MergeIntoParent(ctx context.Context, task api.Task, user api.User) (api.Task, error) {
if task.ParentTaskId == nil || task.WorkflowStage != nil {
if !isSubtask(task) {
return api.Task{}, fmt.Errorf("%w: task %s is not a subtask", ErrInvalid, task.Id)
}
err := m.checkNoSubtasks(ctx, task)
@@ -384,7 +388,7 @@ func (m *Manager) reportToParent(taskID, result string) {
slog.Error("load subtask to report", "task", taskID, "err", err)
return
}
if task.ParentTaskId == nil || task.WorkflowStage != nil || task.MergingIntoParent {
if !isSubtask(task) || task.MergingIntoParent {
return
}
parent, err := m.store.GetTask(ctx, *task.ParentTaskId)
+4 -5
View File
@@ -27,7 +27,6 @@ func (m *Manager) loadUsage() {
m.mu.Lock()
m.usageCommands = cmds
m.mu.Unlock()
go m.readCodexRateLimit()
for _, v := range []api.AgentSessionVendor{api.Claude, api.Codex, api.Antigravity} {
go m.readUsage(v)
}
@@ -67,9 +66,6 @@ func (m *Manager) SetUsageCommands(ctx context.Context, actor string, c api.Usag
m.store.Broadcast(api.Event{Kind: "admin.health", Actor: "byre", Payload: map[string]any{}})
for _, v := range changed {
if v == api.Codex && cmds[v] == "" {
go m.readCodexRateLimit()
}
go m.readUsage(v)
}
return nil
@@ -84,7 +80,10 @@ func (m *Manager) readUsage(vendor api.AgentSessionVendor) {
cmdline := m.usageCommands[vendor]
m.mu.Unlock()
if cmdline == "" {
if vendor == api.Antigravity {
switch vendor {
case api.Codex:
m.readCodexRateLimit()
case api.Antigravity:
m.readAgyRateLimit()
}
return
+29 -41
View File
@@ -28,18 +28,6 @@ const (
var ErrUnavailable = errors.New("client unavailable")
type Client struct {
ID string
Login string
Host string
Commit string
Build string
Device string
ConnectedAt time.Time
LastActionAt *time.Time
Sync *api.ClientSync
}
type Transfer struct {
Dir string
Name string
@@ -54,7 +42,7 @@ type conn struct {
}
type entry struct {
Client
api.ConnectedClient
conn *conn
seen time.Time
notified time.Time
@@ -71,33 +59,34 @@ func New(changed func()) *Registry {
return &Registry{clients: map[string]*entry{}, transfers: map[string]Transfer{}, changed: changed}
}
func (r *Registry) entry(id, login, host, commit, build string, now time.Time) *entry {
func (r *Registry) entry(from api.ConnectedClient, now time.Time) *entry {
for k, e := range r.clients {
if e.conn == nil && now.Sub(e.seen) > expireAfter {
delete(r.clients, k)
}
}
e := r.clients[id]
e := r.clients[from.Id]
if e == nil {
e = &entry{Client: Client{ID: id}}
r.clients[id] = e
e = &entry{ConnectedClient: api.ConnectedClient{Id: from.Id}}
r.clients[from.Id] = e
}
e.Login = login
e.Host = host
e.Commit = commit
e.Build = build
e.Login = from.Login
e.Host = from.Host
e.Device = from.Device
e.Commit = from.Commit
e.Build = from.Build
e.seen = now
return e
}
func (r *Registry) Acted(id, login, host, commit, build string) bool {
if id == "" {
func (r *Registry) Acted(from api.ConnectedClient) bool {
if from.Id == "" {
return false
}
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
e := r.entry(id, login, host, commit, build, now)
e := r.entry(from, now)
e.LastActionAt = &now
if now.Sub(e.notified) < time.Second {
return false
@@ -106,28 +95,27 @@ func (r *Registry) Acted(id, login, host, commit, build string) bool {
return true
}
func (r *Registry) Synced(id, login, host, commit, build string, sync api.ClientSync) {
func (r *Registry) Synced(from api.ConnectedClient, sync api.ClientSync) {
r.mu.Lock()
defer r.mu.Unlock()
r.entry(id, login, host, commit, build, time.Now()).Sync = &sync
r.entry(from, time.Now()).Sync = &sync
}
func (r *Registry) Serve(ctx context.Context, ws *websocket.Conn, id, login, host, commit, build, device string) {
func (r *Registry) Serve(ctx context.Context, ws *websocket.Conn, from api.ConnectedClient) {
ws.SetReadLimit(readLimit)
c := &conn{ws: ws, done: make(chan struct{}), pending: map[string]chan api.ClientReply{}}
r.mu.Lock()
now := time.Now().UTC()
e := r.entry(id, login, host, commit, build, now)
e := r.entry(from, now)
old := e.conn
e.conn = c
e.Device = device
e.ConnectedAt = now
r.mu.Unlock()
if old != nil {
old.ws.Close(websocket.StatusGoingAway, "replaced by a newer connection from this client")
}
slog.Info("client connected", "id", id, "host", host, "device", device, "login", login, "build", build, "commit", commit)
slog.Info("client connected", "id", from.Id, "host", from.Host, "device", from.Device, "login", from.Login, "build", from.Build, "commit", from.Commit)
r.changed()
defer func() {
@@ -138,7 +126,7 @@ func (r *Registry) Serve(ctx context.Context, ws *websocket.Conn, id, login, hos
}
r.mu.Unlock()
close(c.done)
slog.Info("client disconnected", "id", id, "host", host, "login", login)
slog.Info("client disconnected", "id", from.Id, "host", from.Host, "login", from.Login)
r.changed()
}()
@@ -165,7 +153,7 @@ func (r *Registry) Serve(ctx context.Context, ws *websocket.Conn, id, login, hos
}
c.mu.Unlock()
if ch == nil {
slog.Warn("client reply for no request", "host", host, "id", reply.Id)
slog.Warn("client reply for no request", "host", from.Host, "id", reply.Id)
continue
}
ch <- reply
@@ -201,20 +189,20 @@ func (r *Registry) Host(id string) string {
return e.Host
}
func (r *Registry) List() []Client {
func (r *Registry) List() []api.ConnectedClient {
r.mu.Lock()
defer r.mu.Unlock()
out := []Client{}
out := []api.ConnectedClient{}
for _, e := range r.clients {
if e.conn != nil {
out = append(out, e.Client)
out = append(out, e.ConnectedClient)
}
}
sort.Slice(out, func(i, j int) bool { return out[i].ConnectedAt.Before(out[j].ConnectedAt) })
return out
}
func (r *Registry) Recent() []Client {
func (r *Registry) Recent() []api.ConnectedClient {
rows := r.List()
sort.SliceStable(rows, func(i, j int) bool {
a, b := rows[i].LastActionAt, rows[j].LastActionAt
@@ -223,8 +211,8 @@ func (r *Registry) Recent() []Client {
return rows
}
func (r *Registry) RecentMacs() []Client {
out := []Client{}
func (r *Registry) RecentMacs() []api.ConnectedClient {
out := []api.ConnectedClient{}
for _, c := range r.Recent() {
if c.Device == "Mac" {
out = append(out, c)
@@ -241,8 +229,8 @@ func (r *Registry) Call(ctx context.Context, name string, req api.ClientRequest)
if e.conn == nil {
continue
}
connected = append(connected, fmt.Sprintf("%s (%s)", e.Host, e.ID))
if e.ID == name || e.Host == name {
connected = append(connected, fmt.Sprintf("%s (%s)", e.Host, e.Id))
if e.Id == name || e.Host == name {
matches = append(matches, *e)
}
}
@@ -254,7 +242,7 @@ func (r *Registry) Call(ctx context.Context, name string, req api.ClientRequest)
if len(matches) > 1 {
matched := make([]string, len(matches))
for i, e := range matches {
matched[i] = e.ID
matched[i] = e.Id
}
slices.Sort(matched)
return api.ClientReply{}, fmt.Errorf("%d clients on %s are connected; name one by its id: %s", len(matches), name, strings.Join(matched, ", "))
+9 -11
View File
@@ -21,27 +21,25 @@ import (
)
func (s *Server) ConnectClient(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("Byre-Client")
host := clientHost(r)
if id == "" || host == "" {
c := requestClient(r, actor(r))
if c.Id == "" || c.Host == "" {
writeError(w, http.StatusBadRequest, errors.New("missing Byre-Client or Byre-Host, or Byre-Host isn't percent-encoded"))
return
}
ws, err := websocket.Accept(w, r, nil)
if err != nil {
slog.Warn("accept client websocket", "host", host, "err", err)
slog.Warn("accept client websocket", "host", c.Host, "err", err)
return
}
defer ws.CloseNow()
s.clients.Serve(r.Context(), ws, id, actor(r), host, r.Header.Get("Byre-Commit"), r.Header.Get("Byre-Build"), r.Header.Get("Byre-Device"))
s.clients.Serve(r.Context(), ws, c)
}
func (s *Server) SetClientSync(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("Byre-Client")
host := clientHost(r)
if id == "" || host == "" {
c := requestClient(r, actor(r))
if c.Id == "" || c.Host == "" {
writeError(w, http.StatusBadRequest, errors.New("missing Byre-Client or Byre-Host, or Byre-Host isn't percent-encoded"))
return
}
@@ -56,9 +54,9 @@ func (s *Server) SetClientSync(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, errors.New("a failed sync needs an error"))
return
}
slog.Warn("client icloud sync failing", "id", id, "host", host, "error", *sync.Error)
slog.Warn("client icloud sync failing", "id", c.Id, "host", c.Host, "error", *sync.Error)
}
s.clients.Synced(id, actor(r), host, r.Header.Get("Byre-Commit"), r.Header.Get("Byre-Build"), sync)
s.clients.Synced(c, sync)
s.clientsChanged()
w.WriteHeader(http.StatusNoContent)
}
@@ -86,7 +84,7 @@ func (s *Server) ExecClient(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusServiceUnavailable, errors.New("no client Macs are connected"))
return
}
name = recent[0].ID
name = recent[0].Id
}
timeout := time.Duration(req.TimeoutMs) * time.Millisecond
+13 -20
View File
@@ -203,11 +203,22 @@ func (s *Server) identify(next http.Handler) http.Handler {
}
func (s *Server) acted(login string, r *http.Request) {
if s.clients.Acted(r.Header.Get("Byre-Client"), login, clientHost(r), r.Header.Get("Byre-Commit"), r.Header.Get("Byre-Build")) {
if s.clients.Acted(requestClient(r, login)) {
s.clientsChanged()
}
}
func requestClient(r *http.Request, login string) api.ConnectedClient {
return api.ConnectedClient{
Id: r.Header.Get("Byre-Client"),
Login: login,
Host: clientHost(r),
Device: r.Header.Get("Byre-Device"),
Commit: r.Header.Get("Byre-Commit"),
Build: r.Header.Get("Byre-Build"),
}
}
func clientHost(r *http.Request) string {
host, err := url.PathUnescape(r.Header.Get("Byre-Host"))
if err != nil {
@@ -216,24 +227,6 @@ func clientHost(r *http.Request) string {
return host
}
func (s *Server) connectedClients() []api.ConnectedClient {
out := []api.ConnectedClient{}
for _, c := range s.clients.List() {
out = append(out, api.ConnectedClient{
Id: c.ID,
Login: c.Login,
Host: c.Host,
Device: c.Device,
Commit: c.Commit,
Build: c.Build,
ConnectedAt: c.ConnectedAt,
LastActionAt: c.LastActionAt,
Sync: c.Sync,
})
}
return out
}
func (s *Server) clientsChanged() {
s.store.Broadcast(api.Event{Kind: "admin.clients", Actor: "byre", Payload: map[string]any{}})
}
@@ -720,7 +713,7 @@ func (s *Server) status(ctx context.Context) (api.Status, error) {
Projects: c.Projects,
ActiveTasks: c.ActiveTasks,
SseClients: s.hub.count(),
Clients: s.connectedClients(),
Clients: s.clients.List(),
LiveAgents: s.agents.LiveCount(),
RunningTurns: usage.Running,
RateLimits: usage.RateLimits,
+12 -48
View File
@@ -629,9 +629,7 @@ func (s *Store) ParentMergeBy(ctx context.Context, id string) (string, error) {
func (s *Store) ReserveMergeTurn(ctx context.Context, id, sessionID string) (bool, error) {
reserved := false
err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
res, err := tx.ExecContext(ctx, `UPDATE tasks SET merge_state = 'turn' WHERE id = ? AND merge_state = 'claimed'
AND NOT EXISTS (SELECT 1 FROM agent_sessions s WHERE s.task_id = tasks.id AND s.role = ? AND s.state = ?)`,
id, string(api.Implementer), string(api.AgentSessionStateRunning))
res, err := tx.ExecContext(ctx, `UPDATE tasks SET merge_state = 'turn' WHERE id = ? AND merge_state = 'claimed' AND NOT `+implementerRunning, id)
if err != nil {
return err
}
@@ -1339,28 +1337,18 @@ func (s *Store) PausedSessions(ctx context.Context) ([]api.AgentSession, error)
}
func (s *Store) SetSessionPaused(ctx context.Context, id string, paused bool, actor string) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
taskID := ""
projectID := ""
owner := ""
err := tx.QueryRowContext(ctx, `
SELECT coalesce(t.id, ''), coalesce(t.project_id, ''), s.owner FROM agent_sessions s LEFT JOIN tasks t ON t.id = s.task_id WHERE s.id = ?
`, id).Scan(&taskID, &projectID, &owner)
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
}
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `UPDATE agent_sessions SET paused = ? WHERE id = ?`, paused, id)
if err != nil {
return err
}
return emit(sessionEvent("session.paused", projectID, taskID, owner, id, actor, map[string]any{"paused": paused}))
})
return s.updateSession(ctx, id, actor, "session.paused", map[string]any{"paused": paused}, `paused = ?`, paused)
}
func (s *Store) SetSubagents(ctx context.Context, id string, n int, actor string) error {
return s.updateSession(ctx, id, actor, "session.subagents", map[string]any{"subagents": n}, `subagents = ?`, n)
}
func (s *Store) SetSessionModel(ctx context.Context, id, model, effort, actor string) error {
return s.updateSession(ctx, id, actor, "session.model", map[string]any{"model": model, "effort": effort}, `model = ?, effort = ?`, model, effort)
}
func (s *Store) updateSession(ctx context.Context, id, actor, kind string, payload map[string]any, set string, args ...any) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
taskID := ""
projectID := ""
@@ -1374,11 +1362,11 @@ func (s *Store) SetSubagents(ctx context.Context, id string, n int, actor string
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `UPDATE agent_sessions SET subagents = ? WHERE id = ?`, n, id)
_, err = tx.ExecContext(ctx, `UPDATE agent_sessions SET `+set+` WHERE id = ?`, append(args, id)...)
if err != nil {
return err
}
return emit(sessionEvent("session.subagents", projectID, taskID, owner, id, actor, map[string]any{"subagents": n}))
return emit(sessionEvent(kind, projectID, taskID, owner, id, actor, payload))
})
}
@@ -1465,30 +1453,6 @@ func (s *Store) SetVendorSessionID(ctx context.Context, id, vendorSessionID stri
})
}
func (s *Store) SetSessionModel(ctx context.Context, id, model, effort, actor string) error {
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
taskID := ""
projectID := ""
owner := ""
err := tx.QueryRowContext(ctx, `
SELECT coalesce(t.id, ''), coalesce(t.project_id, ''), s.owner FROM agent_sessions s LEFT JOIN tasks t ON t.id = s.task_id WHERE s.id = ?
`, id).Scan(&taskID, &projectID, &owner)
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
}
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `UPDATE agent_sessions SET model = ?, effort = ? WHERE id = ?`, model, effort, id)
if err != nil {
return err
}
return emit(sessionEvent("session.model", projectID, taskID, owner, id, actor, map[string]any{"model": model, "effort": effort}))
})
}
type Counts struct {
LastEventSeq int64
Projects int
+4
View File
@@ -25,6 +25,10 @@ extension ByreTask {
var name: String {
title.isEmpty ? "New task" : title
}
var isSubtask: Bool {
parentTaskId != nil && workflowStage == nil
}
}
extension ByreEvent {
+2 -5
View File
@@ -104,10 +104,7 @@ final class ChatModel {
}
var hasUnmerged: Bool {
guard let f = taskId.flatMap(store.findTask)?.mergeFacts else {
return false
}
return f.unmergedCommits > 0 || f.unmergedFiles > 0
taskId.flatMap(store.findTask)?.mergeFacts.hasChanges ?? false
}
private var feed: Feed? {
@@ -744,7 +741,7 @@ private struct MergeButton: View {
? "Waiting for the merge in progress in this project to finish"
: task.mergeAfterReview
? "Merges when the running review finishes with no findings; open to merge now instead (⌘S)"
: task.parentTaskId != nil && task.workflowStage == nil
: task.isSubtask
? "Merge this subtask into its parent task's branch, or into \(target) (⌘S)"
: "The agent merges \(target) in, then byre squashes into one commit on \(target) and pushes it to origin; held while another task in the project is merging (⌘S)"
Button(intoParent ? "Merging into parent" : held ? "Merge held" : task.mergeAfterReview ? "Merge after review" : keepOpen == false ? "Merge & complete" : "Merge") {
+8 -6
View File
@@ -83,18 +83,20 @@ var modelChoices: [String: String] = [:]
@MainActor
var effortChoices: [String: String] = [:]
func vendorOf(_ model: String) -> String {
typealias Vendor = Components.Schemas.AgentSession.VendorPayload
func vendorOf(_ model: String) -> Vendor {
if model.hasPrefix("gpt-") {
return "codex"
return .codex
}
if model.hasPrefix("gemini-") {
return "antigravity"
return .antigravity
}
return "claude"
return .claude
}
func efforts(_ model: String) -> [String] {
vendorOf(model) == "codex" ? ["", "low", "medium", "high", "xhigh"] : ["", "low", "medium", "high", "xhigh", "max"]
vendorOf(model) == .codex ? ["", "low", "medium", "high", "xhigh"] : ["", "low", "medium", "high", "xhigh", "max"]
}
func effortLabel(_ effort: String) -> String {
@@ -755,7 +757,7 @@ struct ComposerView: View {
let live = chat.closed ? chat.session : chat.liveSession
let current = live.map { $0.model.isEmpty ? chat.builder.context[$0.id]?.model ?? "" : $0.model } ?? ""
let currentEffort = live?.effort ?? ""
let offered = live.map { s in models.filter { vendorOf($0) == s.vendor.rawValue } } ?? models
let offered = live.map { s in models.filter { vendorOf($0) == s.vendor } } ?? models
let options = !current.isEmpty && !offered.contains(current) ? [current] + offered : offered
let modelKey = "message:\(live?.id ?? key)"
let fresh = live == nil ? store.defaultModel : options[0]
+8 -2
View File
@@ -4,6 +4,12 @@ import SwiftUI
typealias Participant = Components.Schemas.Participant
typealias MergeFacts = Components.Schemas.MergeFacts
extension MergeFacts {
var hasChanges: Bool {
unmergedCommits > 0 || unmergedFiles > 0
}
}
@MainActor
private var mergeDrafts: [String: String] = [:]
@@ -67,11 +73,11 @@ func confirmMerge(_ taskId: String, _ store: Store, _ overlays: Overlays, keepOp
return
}
let target = store.projects.first { $0.id == task.projectId }?.defaultBranch ?? "the default branch"
if task.mergeFacts.unmergedCommits == 0 && task.mergeFacts.unmergedFiles == 0 {
if !task.mergeFacts.hasChanges {
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 parent = task.isSubtask ? 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, title: title)
+2 -13
View File
@@ -1,17 +1,6 @@
import ByreAPI
import SwiftUI
let tabLabels: [TaskTab: String] = [
.transcript: "Transcript",
.board: "Board",
.changes: "Changes",
.files: "Files",
.page: "Page",
.captures: "Captures",
.terminal: "Terminal",
.workflow: "Workflow",
]
#if os(macOS)
struct HalfSplit: Layout {
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
@@ -362,7 +351,7 @@ struct TabStrip: View {
}
let run = store.findTask(taskId)?.workflow != nil
let team = !store.team(taskId).isEmpty
let order = taskTabs.filter { t in
let order = TaskTab.allCases.filter { t in
if run {
return t == .workflow
}
@@ -382,7 +371,7 @@ struct TabStrip: View {
var body: some View {
let info = info()
FoldingTabs(tabs: info.order.map { ($0, tabLabels[$0]!) }, active: info.tab) { t in
FoldingTabs(tabs: info.order.map { ($0, $0.label) }, active: info.tab) { t in
store.openTab(t)
}
.padding(.horizontal, 12)
+1 -1
View File
@@ -125,7 +125,7 @@ private struct SheetPanel: View {
private func title(_ task: ByreTask?) -> String {
let label = switch sheet.view {
case .tab(let t): tabLabels[t]!
case .tab(let t): t.label
case .image: "Image"
case .diagram: "Diagram"
}
+6 -4
View File
@@ -2,11 +2,13 @@ import Foundation
import Observation
import SwiftData
enum TaskTab: String {
enum TaskTab: String, CaseIterable {
case transcript, board, changes, files, page, captures, terminal, workflow
}
let taskTabs: [TaskTab] = [.transcript, .board, .changes, .files, .page, .captures, .terminal, .workflow]
var label: String {
rawValue.capitalized
}
}
enum JumpTarget: Equatable {
case question(String)
@@ -614,7 +616,7 @@ final class Store {
let kind = m?[1] ?? nil
let id = m?[2] ?? nil
let name = m?[3] ?? nil
let tab = taskTabs.first { $0.rawValue == name }
let tab = name.flatMap(TaskTab.init(rawValue:))
if kind == "task", let id {
reveals += 1
if let tab, tab != .transcript, tab != .workflow {
+1 -1
View File
@@ -38,7 +38,7 @@ func runGo(_ key: String, _ store: Store, _ overlays: Overlays) {
break
}
Task {
if let task = store.findTask(taskId), task.status == .active, task.state == .discardSuggested || (task.mergeFacts.unmergedCommits == 0 && task.mergeFacts.unmergedFiles == 0) {
if let task = store.findTask(taskId), task.status == .active, task.state == .discardSuggested || !task.mergeFacts.hasChanges {
await confirmDiscard(task, store, overlays)
return
}