Simplify code found by simplicity audit of fa73b74
This commit is contained in:
1 parent
fa73b748c0
commit
8b8551a358
40 files changed
+222
-561
No files matched your search
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"g.fc.run/ai/byre/internal/api"
|
||||
"g.fc.run/ai/byre/internal/shell"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -36,7 +37,7 @@ func clientExecCmd(args []string) {
|
||||
|
||||
words := make([]string, fs.NArg())
|
||||
for i, w := range fs.Args() {
|
||||
words[i] = "'" + strings.ReplaceAll(w, "'", `'\''`) + "'"
|
||||
words[i] = shell.Quote(w)
|
||||
}
|
||||
req := api.ClientExec{Command: strings.Join(words, " "), TimeoutMs: *timeout * 1000}
|
||||
if *client != "" {
|
||||
|
||||
+1
-3
@@ -20,8 +20,6 @@ func main() {
|
||||
checkCmd()
|
||||
case "install-tool":
|
||||
installToolCmd(os.Args[2:])
|
||||
case "run":
|
||||
runCmd(os.Args[2:])
|
||||
case "signing":
|
||||
signingCmd(os.Args[2:])
|
||||
case "grep":
|
||||
@@ -38,7 +36,7 @@ func main() {
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, "usage: byre serve | byre install | byre check | byre install-tool NAME | byre run [--resume ID] | byre signing CHECKOUT | byre grep [--glob GLOB] [--ignore-case] [--files-only] PATTERN PATH | byre list PATH | byre client-exec [--client NAME] [--timeout SECS] -- CMD... | byre screen")
|
||||
fmt.Fprintln(os.Stderr, "usage: byre serve | byre install | byre check | byre install-tool NAME | byre signing CHECKOUT | byre grep [--glob GLOB] [--ignore-case] [--files-only] PATTERN PATH | byre list PATH | byre client-exec [--client NAME] [--timeout SECS] -- CMD... | byre screen")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
|
||||
-173
@@ -1,173 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
|
||||
"g.fc.run/ai/byre/internal/claude"
|
||||
"g.fc.run/ai/byre/internal/git"
|
||||
)
|
||||
|
||||
func runCmd(args []string) {
|
||||
fs := flag.NewFlagSet("run", flag.ExitOnError)
|
||||
resume := fs.String("resume", "", "claude session id to resume")
|
||||
fs.Parse(args)
|
||||
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
fatal("getwd", "err", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
s, err := claude.Start(ctx, claude.Options{
|
||||
Cwd: dir,
|
||||
Resume: *resume,
|
||||
Env: git.IdentityEnv(gitConfig(dir, "user.name"), gitConfig(dir, "user.email")),
|
||||
})
|
||||
if err != nil {
|
||||
fatal("start claude", "err", err)
|
||||
}
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, os.Interrupt)
|
||||
|
||||
lines := make(chan string)
|
||||
go readLines(lines)
|
||||
|
||||
for {
|
||||
fmt.Print("> ")
|
||||
|
||||
select {
|
||||
case line, ok := <-lines:
|
||||
if !ok {
|
||||
shutdown(s)
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
err := s.Send(line)
|
||||
if err != nil {
|
||||
fatal("send", "err", err)
|
||||
}
|
||||
runTurn(ctx, s, sigs)
|
||||
|
||||
case <-sigs:
|
||||
fmt.Println()
|
||||
shutdown(s)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runTurn(ctx context.Context, s *claude.Session, sigs <-chan os.Signal) {
|
||||
for {
|
||||
select {
|
||||
case ev, ok := <-s.Events():
|
||||
if !ok {
|
||||
fatal("claude exited")
|
||||
}
|
||||
if printEvent(ev) {
|
||||
return
|
||||
}
|
||||
|
||||
case <-sigs:
|
||||
fmt.Println("\n[interrupting]")
|
||||
err := s.Interrupt(ctx)
|
||||
if err != nil {
|
||||
slog.Error("interrupt", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printEvent(ev claude.Event) bool {
|
||||
switch {
|
||||
case ev.Type == "system" && ev.Subtype == "init":
|
||||
info := struct {
|
||||
SessionID string `json:"session_id"`
|
||||
Model string `json:"model"`
|
||||
}{}
|
||||
err := json.Unmarshal(ev.Raw, &info)
|
||||
if err != nil {
|
||||
fatal("parse init", "err", err)
|
||||
}
|
||||
fmt.Printf("[session %s, %s]\n", info.SessionID, info.Model)
|
||||
|
||||
case ev.Type == "assistant":
|
||||
blocks, err := ev.Blocks()
|
||||
if err != nil {
|
||||
fatal("parse assistant", "err", err)
|
||||
}
|
||||
for _, b := range blocks {
|
||||
switch b.Type {
|
||||
case "text":
|
||||
fmt.Println(b.Text)
|
||||
case "tool_use":
|
||||
fmt.Printf("→ %s %s\n", b.Name, oneLine(string(b.Input), 160))
|
||||
}
|
||||
}
|
||||
|
||||
case ev.Type == "user":
|
||||
blocks, err := ev.Blocks()
|
||||
if err != nil {
|
||||
fatal("parse user", "err", err)
|
||||
}
|
||||
for _, b := range blocks {
|
||||
if b.Type == "tool_result" && b.IsError {
|
||||
fmt.Printf("← error %s\n", oneLine(string(b.Content), 160))
|
||||
}
|
||||
}
|
||||
|
||||
case ev.Type == "result":
|
||||
res, err := ev.Result()
|
||||
if err != nil {
|
||||
fatal("parse result", "err", err)
|
||||
}
|
||||
fmt.Printf("[%s, %d turns, %.1fs, $%.4f]\n", res.Subtype, res.NumTurns, float64(res.DurationMS)/1000, res.TotalCostUSD)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func readLines(out chan<- string) {
|
||||
defer close(out)
|
||||
sc := bufio.NewScanner(os.Stdin)
|
||||
for sc.Scan() {
|
||||
out <- sc.Text()
|
||||
}
|
||||
}
|
||||
|
||||
func shutdown(s *claude.Session) {
|
||||
err := s.Close()
|
||||
if err != nil {
|
||||
slog.Warn("claude exit", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func gitConfig(dir, key string) string {
|
||||
cmd := exec.Command("git", "config", key)
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
fatal("git config", "key", key, "err", err)
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
func oneLine(s string, max int) string {
|
||||
s = strings.Join(strings.Fields(s), " ")
|
||||
if len(s) > max {
|
||||
return s[:max] + "…"
|
||||
}
|
||||
return s
|
||||
}
|
||||
+3
-1
@@ -7,6 +7,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"g.fc.run/ai/byre/internal/shell"
|
||||
)
|
||||
|
||||
type brewTool struct {
|
||||
@@ -75,7 +77,7 @@ func installCommand(name string) string {
|
||||
if err != nil {
|
||||
fatal("find executable", "err", err)
|
||||
}
|
||||
return "'" + strings.ReplaceAll(exe, "'", `'\''`) + "' install-tool " + name
|
||||
return shell.Quote(exe) + " install-tool " + name
|
||||
}
|
||||
|
||||
func codexHome() string {
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"g.fc.run/ai/byre/internal/api"
|
||||
"g.fc.run/ai/byre/internal/clients"
|
||||
"g.fc.run/ai/byre/internal/mcp"
|
||||
"g.fc.run/ai/byre/internal/shell"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -407,7 +408,7 @@ func (m *Manager) clientOpen(ctx context.Context, l *live, raw json.RawMessage)
|
||||
return "", err
|
||||
}
|
||||
|
||||
command := "open " + dir + "/" + shellQuote(name)
|
||||
command := "open " + dir + "/" + shell.Quote(name)
|
||||
exitCode, output, err := m.runClientBash(ctx, args.Client, command)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -702,10 +703,6 @@ type clientGrepArgs struct {
|
||||
grepArgs
|
||||
}
|
||||
|
||||
func shellQuote(s string) string {
|
||||
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
|
||||
}
|
||||
|
||||
func (m *Manager) clientGrep(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
args := clientGrepArgs{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
@@ -718,7 +715,7 @@ func (m *Manager) clientGrep(ctx context.Context, raw json.RawMessage) (string,
|
||||
}
|
||||
flags := ""
|
||||
if args.Glob != "" {
|
||||
flags += " --glob " + shellQuote(args.Glob)
|
||||
flags += " --glob " + shell.Quote(args.Glob)
|
||||
}
|
||||
if args.IgnoreCase {
|
||||
flags += " --ignore-case"
|
||||
@@ -726,7 +723,7 @@ func (m *Manager) clientGrep(ctx context.Context, raw json.RawMessage) (string,
|
||||
if args.FilesOnly {
|
||||
flags += " --files-only"
|
||||
}
|
||||
return m.runClientByre(ctx, args.Client, "grep", flags+" -- "+shellQuote(args.Pattern)+" "+path)
|
||||
return m.runClientByre(ctx, args.Client, "grep", flags+" -- "+shell.Quote(args.Pattern)+" "+path)
|
||||
}
|
||||
|
||||
type clientListArgs struct {
|
||||
@@ -754,9 +751,9 @@ func clientShellPath(p string) (string, error) {
|
||||
}
|
||||
rest, ok := strings.CutPrefix(p, "~/")
|
||||
if ok {
|
||||
return "~/" + shellQuote(rest), nil
|
||||
return "~/" + shell.Quote(rest), nil
|
||||
}
|
||||
return shellQuote(p), nil
|
||||
return shell.Quote(p), nil
|
||||
}
|
||||
|
||||
func (m *Manager) runClientByre(ctx context.Context, client, sub, args string) (string, error) {
|
||||
|
||||
@@ -558,11 +558,7 @@ func deliveryMessage(pending []store.Pending) string {
|
||||
fmt.Fprintf(b, "%s added: %s\n", who, *r.Text)
|
||||
}
|
||||
case api.Other:
|
||||
text := ""
|
||||
if r.Text != nil {
|
||||
text = *r.Text
|
||||
}
|
||||
fmt.Fprintf(b, "%s answered (none of your options): %s\n", who, text)
|
||||
fmt.Fprintf(b, "%s answered (none of your options): %s\n", who, api.Deref(r.Text))
|
||||
}
|
||||
|
||||
if len(r.Images) > 0 {
|
||||
|
||||
+24
-31
@@ -522,7 +522,7 @@ func (m *Manager) freedText(ctx context.Context, owner string, ev api.Event) (st
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if f.TaskId == nil || *f.TaskId != deref(ev.TaskId) {
|
||||
if f.TaskId == nil || *f.TaskId != api.Deref(ev.TaskId) {
|
||||
return "", nil
|
||||
}
|
||||
finished, err := m.paneFinished(ctx, f)
|
||||
@@ -540,7 +540,7 @@ func (m *Manager) waitingText(ctx context.Context, owner string, ev api.Event) (
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if f.TaskId == nil || *f.TaskId != deref(ev.TaskId) {
|
||||
if f.TaskId == nil || *f.TaskId != api.Deref(ev.TaskId) {
|
||||
return "", nil
|
||||
}
|
||||
waiting, err := m.paneWaiting(ctx, owner, f)
|
||||
@@ -576,10 +576,10 @@ func (m *Manager) signalText(ctx context.Context, ev api.Event) (string, error)
|
||||
return m.itemText(ctx, item), nil
|
||||
|
||||
case "session.error":
|
||||
return fmt.Sprintf("%s hit an error: %v", m.taskLabel(ctx, deref(ev.TaskId)), ev.Payload["error"]), nil
|
||||
return fmt.Sprintf("%s hit an error: %v", m.taskLabel(ctx, api.Deref(ev.TaskId)), ev.Payload["error"]), nil
|
||||
|
||||
case "session.rate_limited":
|
||||
return fmt.Sprintf("%s was rate limited: %v", m.taskLabel(ctx, deref(ev.TaskId)), ev.Payload["error"]), nil
|
||||
return fmt.Sprintf("%s was rate limited: %v", m.taskLabel(ctx, api.Deref(ev.TaskId)), ev.Payload["error"]), nil
|
||||
|
||||
case "task.created":
|
||||
task := api.Task{}
|
||||
@@ -602,50 +602,50 @@ func (m *Manager) signalText(ctx context.Context, ev api.Event) (string, error)
|
||||
return fmt.Sprintf("%s was created by %s from TODO item %q.", m.taskLabel(ctx, task.Id), ev.Actor, *task.TodoItem), nil
|
||||
|
||||
case "task.merge_queued":
|
||||
return fmt.Sprintf("%s's merge was requested by %s and is held until the merge in progress in its project is done.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor), nil
|
||||
return fmt.Sprintf("%s's merge was requested by %s and is held until the merge in progress in its project is done.", m.taskLabel(ctx, api.Deref(ev.TaskId)), ev.Actor), nil
|
||||
|
||||
case "task.merge_after_review":
|
||||
return fmt.Sprintf("%s was set by %s to merge once the reviews of snapshot #%v finish with no findings.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor, ev.Payload["snapshot_n"]), nil
|
||||
return fmt.Sprintf("%s was set by %s to merge once the reviews of snapshot #%v finish with no findings.", m.taskLabel(ctx, api.Deref(ev.TaskId)), ev.Actor, ev.Payload["snapshot_n"]), nil
|
||||
|
||||
case "task.merge_after_review_canceled":
|
||||
return fmt.Sprintf("%s's merge after review was canceled by %s.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor), nil
|
||||
return fmt.Sprintf("%s's merge after review was canceled by %s.", m.taskLabel(ctx, api.Deref(ev.TaskId)), ev.Actor), nil
|
||||
|
||||
case "task.merge_after_review_dropped":
|
||||
return fmt.Sprintf("%s didn't merge after the review: %v", m.taskLabel(ctx, deref(ev.TaskId)), ev.Payload["reason"]), nil
|
||||
return fmt.Sprintf("%s didn't merge after the review: %v", m.taskLabel(ctx, api.Deref(ev.TaskId)), ev.Payload["reason"]), nil
|
||||
|
||||
case "task.merge_started":
|
||||
return fmt.Sprintf("%s is merging, started by %s.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor), nil
|
||||
return fmt.Sprintf("%s is merging, started by %s.", m.taskLabel(ctx, api.Deref(ev.TaskId)), ev.Actor), nil
|
||||
|
||||
case "task.merged":
|
||||
commit, _ := ev.Payload["commit"].(string)
|
||||
if commit == "" {
|
||||
return fmt.Sprintf("%s was merged by %s with nothing to commit.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor), nil
|
||||
return fmt.Sprintf("%s was merged by %s with nothing to commit.", m.taskLabel(ctx, api.Deref(ev.TaskId)), ev.Actor), nil
|
||||
}
|
||||
if into, ok := ev.Payload["parent_task_id"].(string); ok {
|
||||
return fmt.Sprintf("%s was merged by %s into %s, now at %s.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor, m.taskLabel(ctx, into), short(commit)), nil
|
||||
return fmt.Sprintf("%s was merged by %s into %s, now at %s.", m.taskLabel(ctx, api.Deref(ev.TaskId)), ev.Actor, m.taskLabel(ctx, into), short(commit)), nil
|
||||
}
|
||||
return fmt.Sprintf("%s was merged by %s as %s and pushed to %v.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor, short(commit), ev.Payload["pushed"]), nil
|
||||
return fmt.Sprintf("%s was merged by %s as %s and pushed to %v.", m.taskLabel(ctx, api.Deref(ev.TaskId)), ev.Actor, short(commit), ev.Payload["pushed"]), nil
|
||||
|
||||
case "task.discarded":
|
||||
return fmt.Sprintf("%s was discarded by %s.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor), nil
|
||||
return fmt.Sprintf("%s was discarded by %s.", m.taskLabel(ctx, api.Deref(ev.TaskId)), ev.Actor), nil
|
||||
|
||||
case "task.ready":
|
||||
return fmt.Sprintf("%s's agent says the task is ready to merge:\n%s", m.taskLabel(ctx, deref(ev.TaskId)), clip(fmt.Sprint(ev.Payload["overview"]), 3000)), nil
|
||||
return fmt.Sprintf("%s's agent says the task is ready to merge:\n%s", m.taskLabel(ctx, api.Deref(ev.TaskId)), clip(fmt.Sprint(ev.Payload["overview"]), 3000)), nil
|
||||
|
||||
case "task.discard_suggested":
|
||||
return fmt.Sprintf("%s's agent suggests discarding the task:\n%s", m.taskLabel(ctx, deref(ev.TaskId)), clip(fmt.Sprint(ev.Payload["reason"]), 3000)), nil
|
||||
return fmt.Sprintf("%s's agent suggests discarding the task:\n%s", m.taskLabel(ctx, api.Deref(ev.TaskId)), clip(fmt.Sprint(ev.Payload["reason"]), 3000)), nil
|
||||
|
||||
case "task.ready_cleared":
|
||||
if ev.Actor == agentActor {
|
||||
return fmt.Sprintf("%s's agent withdrew its ready for merge or discard suggestion.", m.taskLabel(ctx, deref(ev.TaskId))), nil
|
||||
return fmt.Sprintf("%s's agent withdrew its ready for merge or discard suggestion.", m.taskLabel(ctx, api.Deref(ev.TaskId))), nil
|
||||
}
|
||||
return fmt.Sprintf("%s's ready for merge or discard suggestion was dismissed by %s.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Actor), nil
|
||||
return fmt.Sprintf("%s's ready for merge or discard suggestion was dismissed by %s.", m.taskLabel(ctx, api.Deref(ev.TaskId)), ev.Actor), nil
|
||||
|
||||
case "task.workflow_stage":
|
||||
return fmt.Sprintf("%s started stage %q of workflow %q.", m.taskLabel(ctx, deref(ev.TaskId)), fmt.Sprint(ev.Payload["stage"]), ev.Payload["workflow"]), nil
|
||||
return fmt.Sprintf("%s started stage %q of workflow %q.", m.taskLabel(ctx, api.Deref(ev.TaskId)), fmt.Sprint(ev.Payload["stage"]), ev.Payload["workflow"]), nil
|
||||
|
||||
case "task.workflow_finished":
|
||||
return fmt.Sprintf("%s finished workflow %q:\n%s", m.taskLabel(ctx, deref(ev.TaskId)), ev.Payload["workflow"], clip(fmt.Sprint(ev.Payload["output"]), 1500)), nil
|
||||
return fmt.Sprintf("%s finished workflow %q:\n%s", m.taskLabel(ctx, api.Deref(ev.TaskId)), ev.Payload["workflow"], clip(fmt.Sprint(ev.Payload["output"]), 1500)), nil
|
||||
|
||||
case "review.started", "review.finished":
|
||||
r := api.Review{}
|
||||
@@ -660,21 +660,21 @@ func (m *Manager) signalText(ctx context.Context, ev api.Event) (string, error)
|
||||
|
||||
case "task.state":
|
||||
if ev.Payload["from"] == "" {
|
||||
return fmt.Sprintf("%s state: %v.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Payload["to"]), nil
|
||||
return fmt.Sprintf("%s state: %v.", m.taskLabel(ctx, api.Deref(ev.TaskId)), ev.Payload["to"]), nil
|
||||
}
|
||||
return fmt.Sprintf("%s state: %v → %v.", m.taskLabel(ctx, deref(ev.TaskId)), ev.Payload["from"], ev.Payload["to"]), nil
|
||||
return fmt.Sprintf("%s state: %v → %v.", m.taskLabel(ctx, api.Deref(ev.TaskId)), ev.Payload["from"], ev.Payload["to"]), nil
|
||||
|
||||
case "user.message":
|
||||
return fmt.Sprintf("%s sent %s a message; its agent is working on it:\n%v", ev.Actor, m.taskLabel(ctx, deref(ev.TaskId)), ev.Payload["text"]), nil
|
||||
return fmt.Sprintf("%s sent %s a message; its agent is working on it:\n%v", ev.Actor, m.taskLabel(ctx, api.Deref(ev.TaskId)), ev.Payload["text"]), nil
|
||||
|
||||
case "turn.ended":
|
||||
if ev.Payload["summary"] != true {
|
||||
return "", nil
|
||||
}
|
||||
if member, _ := ev.Payload["member"].(string); member != "" {
|
||||
return fmt.Sprintf("%s's agent %q finished a turn:\n%s", m.taskLabel(ctx, deref(ev.TaskId)), member, clip(fmt.Sprint(ev.Payload["result"]), 1500)), nil
|
||||
return fmt.Sprintf("%s's agent %q finished a turn:\n%s", m.taskLabel(ctx, api.Deref(ev.TaskId)), member, clip(fmt.Sprint(ev.Payload["result"]), 1500)), nil
|
||||
}
|
||||
return fmt.Sprintf("%s finished a turn:\n%s", m.taskLabel(ctx, deref(ev.TaskId)), clip(fmt.Sprint(ev.Payload["result"]), 1500)), nil
|
||||
return fmt.Sprintf("%s finished a turn:\n%s", m.taskLabel(ctx, api.Deref(ev.TaskId)), clip(fmt.Sprint(ev.Payload["result"]), 1500)), nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
@@ -754,10 +754,3 @@ func short(sha string) string {
|
||||
}
|
||||
return sha
|
||||
}
|
||||
|
||||
func deref(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
@@ -170,7 +170,7 @@ func (m *Manager) notesText(ctx context.Context, owner, onlyProject string) (str
|
||||
func writeNotes(b *strings.Builder, notes []api.LeadNote, projectID, heading string) {
|
||||
scoped := []api.LeadNote{}
|
||||
for _, n := range notes {
|
||||
if deref(n.ProjectId) == projectID {
|
||||
if api.Deref(n.ProjectId) == projectID {
|
||||
scoped = append(scoped, n)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,11 +682,11 @@ func (m *Manager) needsHumanElsewhere(ctx context.Context, login, shown string)
|
||||
}
|
||||
|
||||
func sameView(a, b api.Focus) bool {
|
||||
at, bt := deref(a.TaskId), deref(b.TaskId)
|
||||
at, bt := api.Deref(a.TaskId), api.Deref(b.TaskId)
|
||||
if at != "" || bt != "" {
|
||||
return at == bt
|
||||
}
|
||||
return deref(a.ProjectId) == deref(b.ProjectId)
|
||||
return api.Deref(a.ProjectId) == api.Deref(b.ProjectId)
|
||||
}
|
||||
|
||||
func (m *Manager) paneFinished(ctx context.Context, f api.Focus) (string, error) {
|
||||
|
||||
@@ -661,13 +661,7 @@ func (m *Manager) deliverRecorded(ctx context.Context, sess api.AgentSession, ta
|
||||
}
|
||||
text, _ := ev.Payload["text"].(string)
|
||||
client, _ := ev.Payload["client"].(string)
|
||||
ids := []string{}
|
||||
raw, _ := ev.Payload["images"].([]any)
|
||||
for _, id := range raw {
|
||||
s, _ := id.(string)
|
||||
ids = append(ids, s)
|
||||
}
|
||||
imgs, err := m.readImages(ctx, ids)
|
||||
imgs, err := m.readImages(ctx, anyStrings(ev.Payload["images"]))
|
||||
if err != nil {
|
||||
m.failStart(ctx, sess.Id, ev.Actor, err)
|
||||
return true, err
|
||||
|
||||
+19
-21
@@ -410,12 +410,8 @@ func findingsMessage(n int, reviews []api.Review) string {
|
||||
fmt.Fprintf(b, "\n\n## Reviewer %q", r.Stage)
|
||||
for i, f := range findings {
|
||||
fm, _ := f.(map[string]any)
|
||||
where := ""
|
||||
if p, ok := fm["path"].(string); ok && p != "" {
|
||||
where = p
|
||||
if line, ok := fm["line"].(float64); ok && line > 0 {
|
||||
where += fmt.Sprintf(":%d", int(line))
|
||||
}
|
||||
where := findingLocation(fm)
|
||||
if where != "" {
|
||||
where = " (`" + where + "`)"
|
||||
}
|
||||
fmt.Fprintf(b, "\n\n**Finding %d: [%v] %v**%s\n\n%s", i+1, fm["severity"], fm["title"], where, strings.TrimSpace(fmt.Sprint(fm["detail"])))
|
||||
@@ -683,10 +679,7 @@ func reviewLine(r api.Review) string {
|
||||
if r.State == api.ReviewStateSkipped {
|
||||
return fmt.Sprintf("%s skipped: no changes", reviewName(r))
|
||||
}
|
||||
res := map[string]any{}
|
||||
if r.Result != nil {
|
||||
res = *r.Result
|
||||
}
|
||||
res := api.Deref(r.Result)
|
||||
b := &strings.Builder{}
|
||||
fmt.Fprintf(b, "%s: %s", reviewName(r), clip(strings.TrimSpace(fmt.Sprint(res["summary"])), 600))
|
||||
if c, ok := res["change"].(string); ok && c != "" {
|
||||
@@ -697,27 +690,32 @@ func reviewLine(r api.Review) string {
|
||||
}
|
||||
|
||||
func reviewText(r api.Review) string {
|
||||
res := map[string]any{}
|
||||
if r.Result != nil {
|
||||
res = *r.Result
|
||||
}
|
||||
res := api.Deref(r.Result)
|
||||
b := &strings.Builder{}
|
||||
b.WriteString(strings.TrimSpace(fmt.Sprint(res["summary"])))
|
||||
writeFindings(b, res, "\n", 4000)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func findingLocation(finding map[string]any) string {
|
||||
path, _ := finding["path"].(string)
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
line, _ := finding["line"].(float64)
|
||||
if line > 0 {
|
||||
return fmt.Sprintf("%s:%d", path, int(line))
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func writeFindings(b *strings.Builder, res map[string]any, lead string, limit int) {
|
||||
findings, _ := res["findings"].([]any)
|
||||
for _, f := range findings {
|
||||
fm, _ := f.(map[string]any)
|
||||
where := ""
|
||||
if p, ok := fm["path"].(string); ok && p != "" {
|
||||
where = " (" + p
|
||||
if line, ok := fm["line"].(float64); ok && line > 0 {
|
||||
where += fmt.Sprintf(":%d", int(line))
|
||||
}
|
||||
where += ")"
|
||||
where := findingLocation(fm)
|
||||
if where != "" {
|
||||
where = " (" + where + ")"
|
||||
}
|
||||
fmt.Fprintf(b, "%s- [%v] %v%s: %s", lead, fm["severity"], fm["title"], where, clip(strings.TrimSpace(fmt.Sprint(fm["detail"])), limit))
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func (m *Manager) SaveSchedule(ctx context.Context, projectID, scheduleID string
|
||||
if err != nil {
|
||||
return api.Schedule{}, err
|
||||
}
|
||||
sc.Prompt = strings.TrimSpace(deref(save.Prompt))
|
||||
sc.Prompt = strings.TrimSpace(api.Deref(save.Prompt))
|
||||
sc.Workflow = strings.TrimSpace(save.Workflow)
|
||||
_, err = m.store.GetWorkflow(ctx, sc.Workflow)
|
||||
if err != nil {
|
||||
|
||||
@@ -116,13 +116,7 @@ func (m *Manager) WorkflowRun(ctx context.Context, taskID string) (api.WorkflowR
|
||||
for _, ev := range events {
|
||||
switch ev.Kind {
|
||||
case "task.workflow_stage":
|
||||
ids := []string{}
|
||||
list, _ := ev.Payload["after"].([]any)
|
||||
for _, v := range list {
|
||||
id, _ := v.(string)
|
||||
ids = append(ids, id)
|
||||
}
|
||||
after[*ev.TaskId] = ids
|
||||
after[*ev.TaskId] = anyStrings(ev.Payload["after"])
|
||||
case "task.discarded":
|
||||
discardedBy[*ev.TaskId] = ev.Actor
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package api
|
||||
|
||||
func Deref[T any](p *T) T {
|
||||
var zero T
|
||||
if p == nil {
|
||||
return zero
|
||||
}
|
||||
return *p
|
||||
}
|
||||
@@ -17,7 +17,7 @@ func (s *Server) GetTaskChanges(w http.ResponseWriter, r *http.Request, taskID s
|
||||
return
|
||||
}
|
||||
|
||||
step := deref(params.Step)
|
||||
step := api.Deref(params.Step)
|
||||
if params.Snapshot != nil {
|
||||
if params.Since != nil {
|
||||
writeError(w, http.StatusBadRequest, errors.New("since and snapshot can't be used together"))
|
||||
@@ -93,7 +93,7 @@ func (s *Server) GetTaskDiff(w http.ResponseWriter, r *http.Request, taskID stri
|
||||
return
|
||||
}
|
||||
|
||||
patch, err := git.FileDiff(r.Context(), t.WorktreePath, params.Base, params.Tree, params.Path, deref(params.OldPath))
|
||||
patch, err := git.FileDiff(r.Context(), t.WorktreePath, params.Base, params.Tree, params.Path, api.Deref(params.OldPath))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
|
||||
@@ -79,10 +79,7 @@ func (s *Server) ExecClient(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
name := ""
|
||||
if req.Client != nil {
|
||||
name = *req.Client
|
||||
}
|
||||
name := api.Deref(req.Client)
|
||||
if name == "" {
|
||||
recent := s.clients.RecentMacs()
|
||||
if len(recent) == 0 {
|
||||
|
||||
@@ -42,7 +42,7 @@ func (s *Server) GetProjectDir(w http.ResponseWriter, r *http.Request, projectID
|
||||
}
|
||||
|
||||
func serveDir(w http.ResponseWriter, r *http.Request, root string, p *string) {
|
||||
rel := deref(p)
|
||||
rel := api.Deref(p)
|
||||
name, full := taskPath(root, rel)
|
||||
|
||||
f, err := os.Open(full)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func (s *Server) ListInbox(w http.ResponseWriter, r *http.Request, params api.ListInboxParams) {
|
||||
f := store.InboxFilter{State: api.Open, ProjectID: deref(params.ProjectId), TaskID: deref(params.TaskId)}
|
||||
f := store.InboxFilter{State: api.Open, ProjectID: api.Deref(params.ProjectId), TaskID: api.Deref(params.TaskId)}
|
||||
if params.State != nil {
|
||||
f.State = *params.State
|
||||
}
|
||||
|
||||
+2
-14
@@ -4,9 +4,7 @@ import (
|
||||
"errors"
|
||||
"math"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"g.fc.run/ai/byre/internal/agents"
|
||||
"g.fc.run/ai/byre/internal/api"
|
||||
)
|
||||
|
||||
@@ -38,27 +36,17 @@ func (s *Server) ListLeadEvents(w http.ResponseWriter, r *http.Request, params a
|
||||
}
|
||||
|
||||
func (s *Server) MessageLead(w http.ResponseWriter, r *http.Request) {
|
||||
req := api.MessageCreate{}
|
||||
if !readJSON(w, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
images, ok := s.images(w, r, req.Images)
|
||||
prompt, ok := s.readMessage(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.Text) == "" && len(images) == 0 {
|
||||
writeError(w, http.StatusBadRequest, errors.New("text or images are required"))
|
||||
return
|
||||
}
|
||||
|
||||
u, ok := s.actingUser(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
sess, err := s.agents.MessageLead(r.Context(), u, agents.Prompt{Text: req.Text, Model: deref(req.Model), Effort: deref(req.Effort), SendID: deref(req.SendId), Client: r.Header.Get("Byre-Client"), Images: images})
|
||||
sess, err := s.agents.MessageLead(r.Context(), u, prompt)
|
||||
if err != nil {
|
||||
writeAgentError(w, err)
|
||||
return
|
||||
|
||||
+22
-32
@@ -273,14 +273,6 @@ func (s *Server) actingUser(w http.ResponseWriter, r *http.Request) (api.User, b
|
||||
return u, true
|
||||
}
|
||||
|
||||
func deref[T any](p *T) T {
|
||||
var zero T
|
||||
if p == nil {
|
||||
return zero
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
func (s *Server) GetMe(w http.ResponseWriter, r *http.Request) {
|
||||
u, ok := s.actingUser(w, r)
|
||||
if !ok {
|
||||
@@ -393,7 +385,7 @@ func (s *Server) CreateTask(w http.ResponseWriter, r *http.Request, projectID st
|
||||
return
|
||||
}
|
||||
|
||||
title := strings.TrimSpace(deref(req.Title))
|
||||
title := strings.TrimSpace(api.Deref(req.Title))
|
||||
|
||||
p, err := s.store.GetProject(r.Context(), projectID)
|
||||
if err != nil {
|
||||
@@ -441,7 +433,7 @@ func (s *Server) CreateTask(w http.ResponseWriter, r *http.Request, projectID st
|
||||
return
|
||||
}
|
||||
|
||||
prompt := deref(req.Prompt)
|
||||
prompt := api.Deref(req.Prompt)
|
||||
if strings.TrimSpace(prompt) == "" && len(images) == 0 {
|
||||
writeError(w, http.StatusBadRequest, errors.New("prompt is empty"))
|
||||
return
|
||||
@@ -450,7 +442,7 @@ func (s *Server) CreateTask(w http.ResponseWriter, r *http.Request, projectID st
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
t, err := s.agents.StartTask(r.Context(), p, u, title, agents.Prompt{Text: prompt, Model: deref(req.Model), Effort: deref(req.Effort), Client: r.Header.Get("Byre-Client"), Images: images})
|
||||
t, err := s.agents.StartTask(r.Context(), p, u, title, agents.Prompt{Text: prompt, Model: api.Deref(req.Model), Effort: api.Deref(req.Effort), Client: r.Header.Get("Byre-Client"), Images: images})
|
||||
if err != nil {
|
||||
writeAgentError(w, err)
|
||||
return
|
||||
@@ -495,7 +487,7 @@ func (s *Server) StartTodoItem(w http.ResponseWriter, r *http.Request, projectID
|
||||
return
|
||||
}
|
||||
|
||||
tasks, err := s.agents.StartTodo(r.Context(), p, u, req.Title, deref(req.Model), deref(req.Effort), u.Login, "", deref(req.Discuss))
|
||||
tasks, err := s.agents.StartTodo(r.Context(), p, u, req.Title, api.Deref(req.Model), api.Deref(req.Effort), u.Login, "", api.Deref(req.Discuss))
|
||||
if err != nil {
|
||||
writeAgentError(w, err)
|
||||
return
|
||||
@@ -536,8 +528,8 @@ func (s *Server) MergeTask(w http.ResponseWriter, r *http.Request, taskID string
|
||||
}
|
||||
target = *req.Target
|
||||
}
|
||||
afterReview := deref(req.AfterReview)
|
||||
keepOpen := deref(req.KeepOpen)
|
||||
afterReview := api.Deref(req.AfterReview)
|
||||
keepOpen := api.Deref(req.KeepOpen)
|
||||
if keepOpen && (target != api.MergeTargetMain || afterReview) {
|
||||
writeError(w, http.StatusBadRequest, errors.New("keep_open is only for target main, without after_review"))
|
||||
return
|
||||
@@ -830,19 +822,27 @@ func (s *Server) ForkSnapshot(w http.ResponseWriter, r *http.Request, taskID str
|
||||
writeJSON(w, http.StatusCreated, fork)
|
||||
}
|
||||
|
||||
func (s *Server) ContinueSession(w http.ResponseWriter, r *http.Request, sessionID string) {
|
||||
func (s *Server) readMessage(w http.ResponseWriter, r *http.Request) (agents.Prompt, bool) {
|
||||
req := api.MessageCreate{}
|
||||
if !readJSON(w, r, &req) {
|
||||
return
|
||||
return agents.Prompt{}, false
|
||||
}
|
||||
|
||||
images, ok := s.images(w, r, req.Images)
|
||||
if !ok {
|
||||
return
|
||||
return agents.Prompt{}, false
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.Text) == "" && len(images) == 0 {
|
||||
writeError(w, http.StatusBadRequest, errors.New("text or images are required"))
|
||||
return agents.Prompt{}, false
|
||||
}
|
||||
return agents.Prompt{Text: req.Text, Model: api.Deref(req.Model), Effort: api.Deref(req.Effort), SendID: api.Deref(req.SendId), Client: r.Header.Get("Byre-Client"), Images: images}, true
|
||||
}
|
||||
|
||||
func (s *Server) ContinueSession(w http.ResponseWriter, r *http.Request, sessionID string) {
|
||||
prompt, ok := s.readMessage(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -866,7 +866,7 @@ func (s *Server) ContinueSession(w http.ResponseWriter, r *http.Request, session
|
||||
return
|
||||
}
|
||||
|
||||
task, err := s.agents.Continue(r.Context(), src, t, p, u, agents.Prompt{Text: req.Text, Model: deref(req.Model), Effort: deref(req.Effort), SendID: deref(req.SendId), Client: r.Header.Get("Byre-Client"), Images: images})
|
||||
task, err := s.agents.Continue(r.Context(), src, t, p, u, prompt)
|
||||
if err != nil {
|
||||
writeAgentError(w, err)
|
||||
return
|
||||
@@ -914,7 +914,7 @@ func (s *Server) CreateSession(w http.ResponseWriter, r *http.Request, taskID st
|
||||
return
|
||||
}
|
||||
|
||||
sess, err := s.agents.Create(r.Context(), t, u, agents.Prompt{Text: req.Prompt, Model: deref(req.Model), Effort: deref(req.Effort), SendID: deref(req.SendId), Client: r.Header.Get("Byre-Client"), Images: images})
|
||||
sess, err := s.agents.Create(r.Context(), t, u, agents.Prompt{Text: req.Prompt, Model: api.Deref(req.Model), Effort: api.Deref(req.Effort), SendID: api.Deref(req.SendId), Client: r.Header.Get("Byre-Client"), Images: images})
|
||||
if err != nil {
|
||||
writeAgentError(w, err)
|
||||
return
|
||||
@@ -932,22 +932,12 @@ func (s *Server) GetSession(w http.ResponseWriter, r *http.Request, sessionID st
|
||||
}
|
||||
|
||||
func (s *Server) SendMessage(w http.ResponseWriter, r *http.Request, sessionID string) {
|
||||
req := api.MessageCreate{}
|
||||
if !readJSON(w, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
images, ok := s.images(w, r, req.Images)
|
||||
prompt, ok := s.readMessage(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.Text) == "" && len(images) == 0 {
|
||||
writeError(w, http.StatusBadRequest, errors.New("text or images are required"))
|
||||
return
|
||||
}
|
||||
|
||||
sess, _, err := s.agents.Message(r.Context(), sessionID, actor(r), agents.Prompt{Text: req.Text, Model: deref(req.Model), Effort: deref(req.Effort), SendID: deref(req.SendId), Client: r.Header.Get("Byre-Client"), Images: images})
|
||||
sess, _, err := s.agents.Message(r.Context(), sessionID, actor(r), prompt)
|
||||
if err != nil {
|
||||
writeAgentError(w, err)
|
||||
return
|
||||
@@ -969,7 +959,7 @@ func (s *Server) CompactSession(w http.ResponseWriter, r *http.Request, sessionI
|
||||
if !readJSON(w, r, &req) {
|
||||
return
|
||||
}
|
||||
sess, err := s.agents.Compact(r.Context(), sessionID, deref(req.Model), deref(req.Effort))
|
||||
sess, err := s.agents.Compact(r.Context(), sessionID, api.Deref(req.Model), api.Deref(req.Effort))
|
||||
if err != nil {
|
||||
writeAgentError(w, err)
|
||||
return
|
||||
|
||||
@@ -20,8 +20,8 @@ func (s *Server) StreamEvents(w http.ResponseWriter, r *http.Request, params api
|
||||
last = *params.LastEventID
|
||||
}
|
||||
viewer := actor(r)
|
||||
lead := deref(params.Lead)
|
||||
taskID := deref(params.TaskId)
|
||||
lead := api.Deref(params.Lead)
|
||||
taskID := api.Deref(params.TaskId)
|
||||
if params.TaskId != nil {
|
||||
if !s.taskExists(w, r, taskID) {
|
||||
return
|
||||
|
||||
@@ -34,10 +34,7 @@ func (s *Server) PutWorkflow(w http.ResponseWriter, r *http.Request, name string
|
||||
writeError(w, http.StatusBadRequest, fmt.Errorf("read workflow: %w", err))
|
||||
return
|
||||
}
|
||||
from := ""
|
||||
if params.From != nil {
|
||||
from = *params.From
|
||||
}
|
||||
from := api.Deref(params.From)
|
||||
summary, err := s.agents.SaveWorkflow(r.Context(), name, from, body, actor(r))
|
||||
if err != nil {
|
||||
writeAgentError(w, err)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package shell
|
||||
|
||||
import "strings"
|
||||
|
||||
func Quote(s string) string {
|
||||
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
|
||||
}
|
||||
+6
-24
@@ -380,34 +380,16 @@ func (s *Store) CreateTask(ctx context.Context, t api.Task, frozen *FrozenWorkfl
|
||||
workflowBrief = frozen.Brief
|
||||
workflowTitle = frozen.Title
|
||||
}
|
||||
workflowStage := ""
|
||||
if t.WorkflowStage != nil {
|
||||
workflowStage = *t.WorkflowStage
|
||||
}
|
||||
todoItem := ""
|
||||
if t.TodoItem != nil {
|
||||
todoItem = *t.TodoItem
|
||||
}
|
||||
workflowStage := api.Deref(t.WorkflowStage)
|
||||
todoItem := api.Deref(t.TodoItem)
|
||||
mergeTitle := ""
|
||||
if t.MergeTitle != t.Title {
|
||||
mergeTitle = t.MergeTitle
|
||||
}
|
||||
forkedFrom := ""
|
||||
if t.ForkedFrom != nil {
|
||||
forkedFrom = *t.ForkedFrom
|
||||
}
|
||||
forkedAt := 0
|
||||
if t.ForkedAt != nil {
|
||||
forkedAt = *t.ForkedAt
|
||||
}
|
||||
parentTaskID := ""
|
||||
if t.ParentTaskId != nil {
|
||||
parentTaskID = *t.ParentTaskId
|
||||
}
|
||||
audit := ""
|
||||
if t.Audit != nil {
|
||||
audit = *t.Audit
|
||||
}
|
||||
forkedFrom := api.Deref(t.ForkedFrom)
|
||||
forkedAt := api.Deref(t.ForkedAt)
|
||||
parentTaskID := api.Deref(t.ParentTaskId)
|
||||
audit := api.Deref(t.Audit)
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
if parentTaskID != "" {
|
||||
open := false
|
||||
|
||||
@@ -4,7 +4,7 @@ let otherStop = "\u{0}other"
|
||||
|
||||
@MainActor
|
||||
func otherOpen(_ store: Store, _ key: String) -> Bool {
|
||||
(store.drafts?.hasTextAnswer(key) ?? false) || store.images.has(key)
|
||||
store.drafts?.textAnswer(key) != nil || store.images.has(key)
|
||||
}
|
||||
|
||||
struct AttentionCard<Content: View>: View {
|
||||
|
||||
@@ -350,6 +350,16 @@ struct DiagramLayout: Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
private func ancestry(_ id: String, _ boxes: [PlacedBox], _ index: [String: Int]) -> [String] {
|
||||
var out: [String] = []
|
||||
var p = boxes[index[id]!].parent
|
||||
while let id = p {
|
||||
out.append(id)
|
||||
p = boxes[index[id]!].parent
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
struct BoxLayout {
|
||||
let level: String
|
||||
let boxes: [PlacedBox]
|
||||
@@ -359,13 +369,7 @@ struct BoxLayout {
|
||||
let size: CGSize
|
||||
|
||||
func ancestors(_ id: String) -> [String] {
|
||||
var out: [String] = []
|
||||
var p = boxes[index[id]!].parent
|
||||
while let id = p {
|
||||
out.append(id)
|
||||
p = boxes[index[id]!].parent
|
||||
}
|
||||
return out
|
||||
ancestry(id, boxes, index)
|
||||
}
|
||||
|
||||
func opener(at p: CGPoint) -> String? {
|
||||
@@ -802,15 +806,6 @@ private func anchors(_ edges: [BoxEdge], _ points: [[CGPoint]], _ boxes: [Placed
|
||||
let room = min(spread, (s.hi - s.lo) / 4)
|
||||
return v > s.lo + room && v < s.hi - room
|
||||
}
|
||||
func lineage(_ id: String) -> [String] {
|
||||
var out: [String] = []
|
||||
var p = boxes[index[id]!].parent
|
||||
while let id = p {
|
||||
out.append(id)
|
||||
p = boxes[index[id]!].parent
|
||||
}
|
||||
return out
|
||||
}
|
||||
func clear(_ v: CGFloat, _ a: End, _ b: End) -> Bool {
|
||||
let ra = boxes[index[a.box]!].rect
|
||||
let rb = boxes[index[b.box]!].rect
|
||||
@@ -818,7 +813,7 @@ private func anchors(_ edges: [BoxEdge], _ points: [[CGPoint]], _ boxes: [Placed
|
||||
guard gap.width > 0, gap.height > 0 else {
|
||||
return false
|
||||
}
|
||||
let open = Set(lineage(a.box) + lineage(b.box))
|
||||
let open = Set(ancestry(a.box, boxes, index) + ancestry(b.box, boxes, index))
|
||||
return !boxes.contains { box in
|
||||
box.node.id != a.box && box.node.id != b.box && (open.contains(box.node.id) ? box.header : box.rect).intersects(gap)
|
||||
}
|
||||
@@ -970,13 +965,7 @@ private struct Router {
|
||||
}
|
||||
|
||||
private func lineage(_ id: String) -> [String] {
|
||||
var out: [String] = []
|
||||
var p = boxes[index[id]!].parent
|
||||
while let id = p {
|
||||
out.append(id)
|
||||
p = boxes[index[id]!].parent
|
||||
}
|
||||
return out
|
||||
ancestry(id, boxes, index)
|
||||
}
|
||||
|
||||
private func blocked(_ e: BoxEdge) -> (grid: [Bool], upright: [Bool], level: [Bool]) {
|
||||
|
||||
@@ -717,12 +717,7 @@ private struct ChatHead: View {
|
||||
HStack(spacing: 8) {
|
||||
content()
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
.overlay(alignment: .bottom) {
|
||||
Rectangle().fill(Palette.border).frame(height: 1)
|
||||
}
|
||||
.modifier(HeaderBar())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -845,7 +840,6 @@ struct CompactButton: View {
|
||||
let usage: Usage?
|
||||
@Environment(Store.self) private var store
|
||||
@Environment(Overlays.self) private var overlays
|
||||
@Environment(\.isEnabled) private var enabled
|
||||
@ViewState private var sending = false
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -272,10 +272,6 @@ final class Drafts {
|
||||
save()
|
||||
}
|
||||
|
||||
func hasTextAnswer(_ id: String) -> Bool {
|
||||
texts[id] != nil
|
||||
}
|
||||
|
||||
func textAnswer(_ id: String) -> String? {
|
||||
texts[id]
|
||||
}
|
||||
|
||||
@@ -467,7 +467,6 @@ private struct WebButton: View {
|
||||
|
||||
struct SmallLinkButtonStyle: ButtonStyle {
|
||||
var color = Palette.accent
|
||||
var border = Palette.border
|
||||
@Environment(\.isEnabled) private var enabled
|
||||
|
||||
func makeBody(configuration: ButtonStyleConfiguration) -> some View {
|
||||
@@ -476,7 +475,7 @@ struct SmallLinkButtonStyle: ButtonStyle {
|
||||
.foregroundStyle(color)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, touchOnly ? 3 : 0)
|
||||
.overlay(RoundedRectangle(cornerRadius: 4).stroke(border))
|
||||
.overlay(RoundedRectangle(cornerRadius: 4).stroke(Palette.border))
|
||||
.padding(.vertical, touchOnly ? 5 : 0)
|
||||
.opacity(enabled ? 1 : 0.45)
|
||||
.contentShape(Rectangle())
|
||||
@@ -484,7 +483,6 @@ struct SmallLinkButtonStyle: ButtonStyle {
|
||||
}
|
||||
|
||||
struct FieldStyle: ViewModifier {
|
||||
var accent: Color? = nil
|
||||
var focused = false
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
@@ -493,7 +491,41 @@ struct FieldStyle: ViewModifier {
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background(Palette.raised, in: RoundedRectangle(cornerRadius: 6))
|
||||
.overlay(RoundedRectangle(cornerRadius: 6).stroke(focused ? (accent ?? Palette.accent) : (accent ?? Palette.border)))
|
||||
.overlay(RoundedRectangle(cornerRadius: 6).stroke(focused ? Palette.accent : Palette.border))
|
||||
}
|
||||
}
|
||||
|
||||
struct DropdownMenu<Items: View>: View {
|
||||
let label: String
|
||||
var boxed = false
|
||||
var size: CGFloat = 12
|
||||
var color = Palette.text
|
||||
@ViewBuilder let items: Items
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
items
|
||||
} label: {
|
||||
let row = HStack(spacing: boxed ? 8 : 6) {
|
||||
Text(label)
|
||||
Image(systemName: "chevron.up.chevron.down")
|
||||
.font(Fonts.font(boxed ? 9 : 8, weight: 600))
|
||||
}
|
||||
.font(boxed ? nil : Fonts.font(size))
|
||||
.foregroundStyle(color)
|
||||
if boxed {
|
||||
row
|
||||
.modifier(FieldStyle())
|
||||
.contentShape(Rectangle())
|
||||
} else {
|
||||
row
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
.menuStyle(.button)
|
||||
.buttonStyle(.plain)
|
||||
.menuIndicator(.hidden)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,7 +664,7 @@ struct StretchColumn: Layout {
|
||||
}
|
||||
|
||||
struct TrailingBubble: Layout {
|
||||
var fraction: CGFloat = 0.85
|
||||
private let fraction: CGFloat = 0.85
|
||||
|
||||
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
||||
let width = proposal.width ?? 600
|
||||
|
||||
@@ -6,51 +6,39 @@ enum MDSegment {
|
||||
}
|
||||
|
||||
enum AppLink {
|
||||
static func task(_ id: String, tab: String? = nil) -> URL {
|
||||
route("#/task/\(id)\(tab.map { "/\($0)" } ?? "")")
|
||||
private static func link(_ host: String, _ items: KeyValuePairs<String, String>) -> URL {
|
||||
var c = URLComponents()
|
||||
c.scheme = "byre-app"
|
||||
c.host = host
|
||||
c.queryItems = items.map { URLQueryItem(name: $0.key, value: $0.value) }
|
||||
return c.url!
|
||||
}
|
||||
|
||||
static func task(_ id: String) -> URL {
|
||||
route("#/task/\(id)")
|
||||
}
|
||||
|
||||
static func route(_ hash: String) -> URL {
|
||||
var c = URLComponents()
|
||||
c.scheme = "byre-app"
|
||||
c.host = "route"
|
||||
c.queryItems = [URLQueryItem(name: "hash", value: hash)]
|
||||
return c.url!
|
||||
link("route", ["hash": hash])
|
||||
}
|
||||
|
||||
static func file(_ root: FileRoot, _ path: String) -> URL {
|
||||
var c = URLComponents()
|
||||
c.scheme = "byre-app"
|
||||
c.host = "file"
|
||||
c.queryItems = switch root {
|
||||
case .task(let id): [URLQueryItem(name: "task", value: id), URLQueryItem(name: "path", value: path)]
|
||||
case .project(let id): [URLQueryItem(name: "project", value: id), URLQueryItem(name: "path", value: path)]
|
||||
switch root {
|
||||
case .task(let id): link("file", ["task": id, "path": path])
|
||||
case .project(let id): link("file", ["project": id, "path": path])
|
||||
}
|
||||
return c.url!
|
||||
}
|
||||
|
||||
static func preview(_ taskId: String, _ port: Int) -> URL {
|
||||
var c = URLComponents()
|
||||
c.scheme = "byre-app"
|
||||
c.host = "preview"
|
||||
c.queryItems = [URLQueryItem(name: "task", value: taskId), URLQueryItem(name: "port", value: String(port))]
|
||||
return c.url!
|
||||
link("preview", ["task": taskId, "port": String(port)])
|
||||
}
|
||||
|
||||
static func todo(_ projectId: String, _ title: String, discuss: Bool) -> URL {
|
||||
var c = URLComponents()
|
||||
c.scheme = "byre-app"
|
||||
c.host = "todo"
|
||||
c.queryItems = [URLQueryItem(name: "project", value: projectId), URLQueryItem(name: "title", value: title), URLQueryItem(name: "discuss", value: discuss ? "1" : "0")]
|
||||
return c.url!
|
||||
link("todo", ["project": projectId, "title": title, "discuss": discuss ? "1" : "0"])
|
||||
}
|
||||
|
||||
static func suggest(_ projectId: String, _ title: String, _ brief: String, discuss: Bool) -> URL {
|
||||
var c = URLComponents()
|
||||
c.scheme = "byre-app"
|
||||
c.host = "suggest"
|
||||
c.queryItems = [URLQueryItem(name: "project", value: projectId), URLQueryItem(name: "title", value: title), URLQueryItem(name: "brief", value: brief), URLQueryItem(name: "discuss", value: discuss ? "1" : "0")]
|
||||
return c.url!
|
||||
link("suggest", ["project": projectId, "title": title, "brief": brief, "discuss": discuss ? "1" : "0"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,7 +416,7 @@ struct InlineRenderer {
|
||||
}
|
||||
render(inner, style, &out)
|
||||
append(run(" ", style), &out)
|
||||
let busy = store.todoBusy["\(projectId)\n\(title)"]
|
||||
let busy = store.todoBusy(projectId, title)
|
||||
func action(_ discuss: Bool, _ label: String, _ busyLabel: String, _ tip: String) {
|
||||
let url = brief.map { AppLink.suggest(projectId, title, $0, discuss: discuss) } ?? AppLink.todo(projectId, title, discuss: discuss)
|
||||
let mine = busy == (discuss ? "discuss" : "start")
|
||||
@@ -1080,14 +1068,15 @@ private struct DiffBlock: View {
|
||||
ScrollView(.horizontal) {
|
||||
StretchColumn(minWidth: width) {
|
||||
ForEach(Array(lines.enumerated()), id: \.offset) { _, line in
|
||||
let kind = line.hasPrefix("+++") || line.hasPrefix("---") ? PatchLine.meta : PatchLine(line)
|
||||
Text(verbatim: line.isEmpty ? " " : line)
|
||||
.font(Fonts.font(size, mono: true))
|
||||
.foregroundStyle(line.hasPrefix("@@") || line.hasPrefix("+++") || line.hasPrefix("---") ? Palette.muted : Palette.text)
|
||||
.foregroundStyle(kind.color)
|
||||
.textSelection(.enabled)
|
||||
.fixedSize()
|
||||
.padding(.horizontal, 8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(background(line))
|
||||
.background(kind.background)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 6)
|
||||
@@ -1099,22 +1088,6 @@ private struct DiffBlock: View {
|
||||
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Palette.border))
|
||||
}
|
||||
}
|
||||
|
||||
private func background(_ l: String) -> Color {
|
||||
if l.hasPrefix("@@") {
|
||||
return Palette.surface
|
||||
}
|
||||
if l.hasPrefix("+++") || l.hasPrefix("---") {
|
||||
return .clear
|
||||
}
|
||||
if l.hasPrefix("+") {
|
||||
return Palette.ok.opacity(0.14)
|
||||
}
|
||||
if l.hasPrefix("-") {
|
||||
return Palette.danger.opacity(0.14)
|
||||
}
|
||||
return .clear
|
||||
}
|
||||
}
|
||||
|
||||
private struct DetailsBlock: View {
|
||||
|
||||
@@ -14,7 +14,6 @@ typealias Leave = @MainActor (@escaping @MainActor () -> Void) -> Void
|
||||
struct MergeWarning {
|
||||
let text: String
|
||||
var open: (@MainActor () -> Void)?
|
||||
var detail = ""
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -287,7 +286,6 @@ private struct MergeContent: View {
|
||||
} else {
|
||||
Text("⚠ \(w.text)")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.help(w.detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ struct NotesView: View {
|
||||
|
||||
private struct LeadNotes: View {
|
||||
@Environment(Store.self) private var store
|
||||
@Environment(Overlays.self) private var overlays
|
||||
@ViewState private var list: [LeadNote]?
|
||||
@ViewState private var failure: String?
|
||||
@ViewState private var loads = 0
|
||||
@@ -45,27 +44,14 @@ private struct LeadNotes: View {
|
||||
let current = groups.contains { $0.0 == project } ? project : ""
|
||||
let shown = list.filter { ($0.projectId ?? "") == current }
|
||||
let label = { (g: (String, String)) in "\(g.1) (\(list.filter { ($0.projectId ?? "") == g.0 }.count))" }
|
||||
Menu {
|
||||
DropdownMenu(label: label(groups.first { $0.0 == current } ?? groups[0]), boxed: true) {
|
||||
ForEach(groups, id: \.0) { g in
|
||||
Button(label(g)) {
|
||||
project = g.0
|
||||
notesProject = g.0
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
Text(label(groups.first { $0.0 == current } ?? groups[0]))
|
||||
Image(systemName: "chevron.up.chevron.down")
|
||||
.font(Fonts.font(9, weight: 600))
|
||||
}
|
||||
.foregroundStyle(Palette.text)
|
||||
.modifier(FieldStyle())
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.menuStyle(.button)
|
||||
.buttonStyle(.plain)
|
||||
.menuIndicator(.hidden)
|
||||
.fixedSize()
|
||||
.help("Which notes to show")
|
||||
.padding(.bottom, 8)
|
||||
if shown.isEmpty {
|
||||
|
||||
@@ -73,16 +73,16 @@ final class PaneFocus {
|
||||
let center = NotificationCenter.default
|
||||
observers = [
|
||||
center.addObserver(of: window, for: .didBecomeKey) { [weak self] _ in
|
||||
self?.changed(NSWindow.didBecomeKeyNotification)
|
||||
self?.update()
|
||||
},
|
||||
center.addObserver(of: window, for: .didResignKey) { [weak self] _ in
|
||||
self?.changed(NSWindow.didResignKeyNotification)
|
||||
self?.update()
|
||||
},
|
||||
center.addObserver(of: NSApplication.self, for: .didBecomeActive) { [weak self] _ in
|
||||
self?.changed(NSApplication.didBecomeActiveNotification)
|
||||
self?.update()
|
||||
},
|
||||
center.addObserver(of: NSApplication.self, for: .didResignActive) { [weak self] _ in
|
||||
self?.changed(NSApplication.didResignActiveNotification)
|
||||
self?.update()
|
||||
},
|
||||
]
|
||||
responder = window.observe(\.firstResponder, options: [.new]) { [weak self] _, _ in
|
||||
@@ -183,10 +183,6 @@ final class PaneFocus {
|
||||
return CGPoint(x: r.midX, y: content.isFlipped ? r.midY : content.bounds.height - r.midY)
|
||||
}
|
||||
|
||||
private func changed(_ name: Notification.Name) {
|
||||
update()
|
||||
}
|
||||
|
||||
private func responderChanged() {
|
||||
guard let view = window?.firstResponder as? NSView, view !== window?.contentView, !view.visibleRect.isEmpty else {
|
||||
update()
|
||||
|
||||
@@ -112,25 +112,12 @@ private struct ProjectWorkflowMenu: View {
|
||||
|
||||
var body: some View {
|
||||
let current = audit ? project.auditWorkflow : project.reviewWorkflow
|
||||
Menu {
|
||||
DropdownMenu(label: current.isEmpty ? "Off" : current, size: 11, color: Palette.muted) {
|
||||
Button("Off") { set("") }
|
||||
ForEach(workflows, id: \.self) { name in
|
||||
Button(name) { set(name) }
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
Text(current.isEmpty ? "Off" : current)
|
||||
Image(systemName: "chevron.up.chevron.down")
|
||||
.font(Fonts.font(8, weight: 600))
|
||||
}
|
||||
.font(Fonts.font(11))
|
||||
.foregroundStyle(Palette.muted)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.menuStyle(.button)
|
||||
.buttonStyle(.plain)
|
||||
.menuIndicator(.hidden)
|
||||
.fixedSize()
|
||||
.help(audit ? "The workflow byre runs in the background on \(project.defaultBranch)'s newest commit to check the whole codebase" : "The review workflow whose reviewers review every snapshot of this project's tasks")
|
||||
.task {
|
||||
do {
|
||||
@@ -633,7 +620,7 @@ private struct TodoRow: View {
|
||||
.buttonStyle(.plain)
|
||||
.fixedSize()
|
||||
} else {
|
||||
let busy = store.todoStarting.contains("\(project.id)\n\(item.title)")
|
||||
let busy = store.todoBusy(project.id, item.title) != nil
|
||||
HStack(spacing: 6) {
|
||||
Button(busy ? "Starting…" : "Start") {
|
||||
store.startTodo(project.id, item.title, discuss: false, overlays)
|
||||
|
||||
@@ -10,7 +10,7 @@ struct QuickRunSetting: View {
|
||||
var body: some View {
|
||||
let current = store.me?.quickRun ?? .init(projectId: "", workflow: "")
|
||||
HStack(spacing: 6) {
|
||||
menu(current.workflow.isEmpty ? "None" : current.workflow, help: "Workflow the Action button starts") {
|
||||
DropdownMenu(label: current.workflow.isEmpty ? "None" : current.workflow, boxed: true) {
|
||||
Button("None") {
|
||||
save(.init(projectId: "", workflow: ""))
|
||||
}
|
||||
@@ -20,16 +20,18 @@ struct QuickRunSetting: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.help("Workflow the Action button starts")
|
||||
if !current.workflow.isEmpty {
|
||||
Text("in")
|
||||
.foregroundStyle(Palette.muted)
|
||||
menu(store.projects.first { $0.id == current.projectId }?.name ?? current.projectId, help: "Project the workflow runs in") {
|
||||
DropdownMenu(label: store.projects.first { $0.id == current.projectId }?.name ?? current.projectId, boxed: true) {
|
||||
ForEach(store.projects, id: \.id) { p in
|
||||
Button(p.name) {
|
||||
save(.init(projectId: p.id, workflow: current.workflow))
|
||||
}
|
||||
}
|
||||
}
|
||||
.help("Project the workflow runs in")
|
||||
}
|
||||
}
|
||||
.padding(.top, 8)
|
||||
@@ -47,26 +49,6 @@ struct QuickRunSetting: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func menu(_ label: String, help: String, @ViewBuilder _ items: () -> some View) -> some View {
|
||||
Menu {
|
||||
items()
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
Text(label)
|
||||
Image(systemName: "chevron.up.chevron.down")
|
||||
.font(Fonts.font(9, weight: 600))
|
||||
}
|
||||
.foregroundStyle(Palette.text)
|
||||
.modifier(FieldStyle())
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.menuStyle(.button)
|
||||
.buttonStyle(.plain)
|
||||
.menuIndicator(.hidden)
|
||||
.fixedSize()
|
||||
.help(help)
|
||||
}
|
||||
|
||||
private func load() {
|
||||
Task {
|
||||
do {
|
||||
|
||||
@@ -431,7 +431,7 @@ private struct PickMenu: View {
|
||||
@Environment(Overlays.self) private var overlays
|
||||
|
||||
var body: some View {
|
||||
Menu {
|
||||
DropdownMenu(label: label(current), boxed: true) {
|
||||
ForEach(options, id: \.self) { m in
|
||||
Button(label(m)) {
|
||||
Task {
|
||||
@@ -443,20 +443,7 @@ private struct PickMenu: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
Text(label(current))
|
||||
Image(systemName: "chevron.up.chevron.down")
|
||||
.font(Fonts.font(9, weight: 600))
|
||||
}
|
||||
.foregroundStyle(Palette.text)
|
||||
.modifier(FieldStyle())
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.menuStyle(.button)
|
||||
.buttonStyle(.plain)
|
||||
.menuIndicator(.hidden)
|
||||
.fixedSize()
|
||||
.help(help)
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,7 @@ final class Store {
|
||||
var composerFocus: String?
|
||||
var summaryReads: [String: Int64] = [:]
|
||||
private(set) var leadMore = false
|
||||
var todoBusy: [String: String] = [:]
|
||||
private var todoBusyKinds: [String: String] = [:]
|
||||
var pickedMembers: [String: String] = [:]
|
||||
var filePlaces: [FileRoot: FilePlace] = [:]
|
||||
private(set) var drafts: Drafts?
|
||||
@@ -158,57 +158,43 @@ final class Store {
|
||||
drafts!.prune(self)
|
||||
}
|
||||
|
||||
var todoStarting: Set<String> {
|
||||
Set(todoBusy.keys)
|
||||
func todoBusy(_ projectId: String, _ title: String) -> String? {
|
||||
todoBusyKinds["\(projectId)\n\(title)"]
|
||||
}
|
||||
|
||||
func startTodo(_ projectId: String, _ title: String, discuss: Bool, _ overlays: Overlays) {
|
||||
private func busyTodo(_ projectId: String, _ title: String, _ kind: String, _ overlays: Overlays, _ op: @escaping @MainActor () async throws -> Void) {
|
||||
let key = "\(projectId)\n\(title)"
|
||||
if todoBusy[key] != nil {
|
||||
if todoBusyKinds[key] != nil {
|
||||
return
|
||||
}
|
||||
todoBusy[key] = discuss ? "discuss" : "start"
|
||||
todoBusyKinds[key] = kind
|
||||
Task {
|
||||
do {
|
||||
_ = try await api.client.startTodoItem(path: .init(projectId: projectId), body: .json(.init(title: title, model: defaultModel, effort: defaultEffort, discuss: discuss))).created
|
||||
try await op()
|
||||
} catch {
|
||||
overlays.showAlert(errorText(error))
|
||||
}
|
||||
todoBusy[key] = nil
|
||||
todoBusyKinds[key] = nil
|
||||
}
|
||||
}
|
||||
|
||||
func startTodo(_ projectId: String, _ title: String, discuss: Bool, _ overlays: Overlays) {
|
||||
busyTodo(projectId, title, discuss ? "discuss" : "start", overlays) {
|
||||
_ = try await self.api.client.startTodoItem(path: .init(projectId: projectId), body: .json(.init(title: title, model: self.defaultModel, effort: self.defaultEffort, discuss: discuss))).created
|
||||
}
|
||||
}
|
||||
|
||||
func startSuggestion(_ projectId: String, _ title: String, brief: String, _ overlays: Overlays) {
|
||||
let key = "\(projectId)\n\(title)"
|
||||
if todoBusy[key] != nil {
|
||||
return
|
||||
}
|
||||
todoBusy[key] = "start"
|
||||
Task {
|
||||
do {
|
||||
_ = try await api.client.createTask(path: .init(projectId: projectId), body: .json(.init(title: title, prompt: brief, model: defaultModel, effort: defaultEffort))).created
|
||||
} catch {
|
||||
overlays.showAlert(errorText(error))
|
||||
}
|
||||
todoBusy[key] = nil
|
||||
busyTodo(projectId, title, "start", overlays) {
|
||||
_ = try await self.api.client.createTask(path: .init(projectId: projectId), body: .json(.init(title: title, prompt: brief, model: self.defaultModel, effort: self.defaultEffort))).created
|
||||
}
|
||||
}
|
||||
|
||||
func discussSuggestion(_ projectId: String, _ title: String, brief: String, _ overlays: Overlays) {
|
||||
let key = "\(projectId)\n\(title)"
|
||||
if todoBusy[key] != nil {
|
||||
return
|
||||
}
|
||||
todoBusy[key] = "discuss"
|
||||
let lead = currentLead()
|
||||
let live = lead != nil && lead!.state != .completed
|
||||
Task {
|
||||
do {
|
||||
_ = try await api.client.messageLead(body: .json(.init(text: "Let's discuss your suggestion \"\(title)\" before starting it:\n\n\(brief)", model: live ? nil : defaultModel, effort: live ? nil : defaultEffort))).accepted
|
||||
} catch {
|
||||
overlays.showAlert(errorText(error))
|
||||
}
|
||||
todoBusy[key] = nil
|
||||
busyTodo(projectId, title, "discuss", overlays) {
|
||||
_ = try await self.api.client.messageLead(body: .json(.init(text: "Let's discuss your suggestion \"\(title)\" before starting it:\n\n\(brief)", model: live ? nil : self.defaultModel, effort: live ? nil : self.defaultEffort))).accepted
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -171,7 +171,6 @@ struct PendingSend: Identifiable {
|
||||
let text: String
|
||||
let images: [PendingImage]
|
||||
let imageIds: [String]
|
||||
let local: Bool
|
||||
let queued: Bool
|
||||
let md: [MDBlock]
|
||||
let sendID: String
|
||||
@@ -286,7 +285,7 @@ final class TranscriptBuilder {
|
||||
// The returned closure drops the send and says whether it was still unconfirmed.
|
||||
func showPending(_ text: String, _ images: [PendingImage], _ sendID: String) -> () -> Bool {
|
||||
local.ids -= 1
|
||||
let entry = PendingSend(id: local.ids, text: text, images: images, imageIds: [], local: true, queued: running(), md: trimmed(text).isEmpty ? [] : parseMarkdown(text), sendID: sendID)
|
||||
let entry = PendingSend(id: local.ids, text: text, images: images, imageIds: [], queued: running(), md: trimmed(text).isEmpty ? [] : parseMarkdown(text), sendID: sendID)
|
||||
local.items.append(entry)
|
||||
ctx.run = nil
|
||||
return { [local] in
|
||||
@@ -499,7 +498,7 @@ final class TranscriptBuilder {
|
||||
let text = str(p, "text") ?? ""
|
||||
pendingIds += 1
|
||||
let images = (p["images"] as? [(any Sendable)?] ?? []).map { jsString($0) }
|
||||
queued.append(PendingSend(id: pendingIds, text: text, images: [], imageIds: images, local: false, queued: true, md: trimmed(text).isEmpty ? [] : parseMarkdown(text), sendID: str(p, "send_id") ?? "", inputId: int(p, "id")))
|
||||
queued.append(PendingSend(id: pendingIds, text: text, images: [], imageIds: images, queued: true, md: trimmed(text).isEmpty ? [] : parseMarkdown(text), sendID: str(p, "send_id") ?? "", inputId: int(p, "id")))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@ private func taskLink(_ taskId: String, _ label: String, bold: Bool = false) ->
|
||||
private func stateColor(_ state: String) -> Color {
|
||||
switch state {
|
||||
case "running": Palette.accent
|
||||
case "idle", "discarding": Palette.muted
|
||||
case "finished", "merged": Palette.ok
|
||||
default: Palette.muted
|
||||
}
|
||||
|
||||
@@ -572,24 +572,11 @@ struct ChoiceMenu<Value: Hashable>: View {
|
||||
|
||||
var body: some View {
|
||||
let label = options.first { $0.0 == value }?.1
|
||||
Menu {
|
||||
DropdownMenu(label: label ?? placeholder, color: label == nil ? Palette.muted : Palette.text) {
|
||||
ForEach(options, id: \.0) { option in
|
||||
Button(option.1) { value = option.0 }
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
Text(label ?? placeholder)
|
||||
Image(systemName: "chevron.up.chevron.down")
|
||||
.font(Fonts.font(8, weight: 600))
|
||||
}
|
||||
.font(Fonts.font(12))
|
||||
.foregroundStyle(label == nil ? Palette.muted : Palette.text)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.menuStyle(.button)
|
||||
.buttonStyle(.plain)
|
||||
.menuIndicator(.hidden)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user