180 lines
4.1 KiB
Go
180 lines
4.1 KiB
Go
package control
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"reasonix/internal/agent"
|
|
)
|
|
|
|
// ParseBranchTarget parses the arguments after "/branch". A leading positive
|
|
// integer means "branch from displayed turn N"; otherwise the whole argument is
|
|
// the optional branch name for a tip branch.
|
|
func ParseBranchTarget(args string) (turn int, name string, fromTurn bool, err error) {
|
|
args = strings.TrimSpace(args)
|
|
fields := strings.Fields(args)
|
|
if len(fields) == 0 {
|
|
return 0, "", false, nil
|
|
}
|
|
n, convErr := strconv.Atoi(fields[0])
|
|
if convErr != nil {
|
|
return 0, args, false, nil
|
|
}
|
|
if n <= 0 {
|
|
return 0, "", false, fmt.Errorf("usage: /branch [turn] [name]")
|
|
}
|
|
name = strings.TrimSpace(strings.TrimPrefix(args, fields[0]))
|
|
return n, name, true, nil
|
|
}
|
|
|
|
func (c *Controller) BranchTreeText() string {
|
|
branches, err := c.Branches()
|
|
if err != nil {
|
|
return "branches: " + err.Error()
|
|
}
|
|
return FormatBranchTree(branches, agent.BranchID(c.SessionPath()))
|
|
}
|
|
|
|
func FormatBranchTree(branches []agent.BranchInfo, currentID string) string {
|
|
if len(branches) == 0 {
|
|
return "branches: none"
|
|
}
|
|
byID := map[string]agent.BranchInfo{}
|
|
children := map[string][]agent.BranchInfo{}
|
|
for _, b := range branches {
|
|
byID[b.ID] = b
|
|
}
|
|
var roots []agent.BranchInfo
|
|
for _, b := range branches {
|
|
if b.ParentID == "" {
|
|
roots = append(roots, b)
|
|
continue
|
|
}
|
|
if _, ok := byID[b.ParentID]; !ok {
|
|
roots = append(roots, b)
|
|
continue
|
|
}
|
|
children[b.ParentID] = append(children[b.ParentID], b)
|
|
}
|
|
var out strings.Builder
|
|
out.WriteString("branches:\n")
|
|
seen := map[string]bool{}
|
|
var walk func(agent.BranchInfo, string, bool, int)
|
|
walk = func(b agent.BranchInfo, prefix string, last bool, depth int) {
|
|
if seen[b.ID] {
|
|
return
|
|
}
|
|
seen[b.ID] = true
|
|
joint := "├─"
|
|
childPrefix := prefix + "│ "
|
|
if last {
|
|
joint = "└─"
|
|
childPrefix = prefix + " "
|
|
}
|
|
current := ""
|
|
if b.ID == currentID {
|
|
current = " current"
|
|
}
|
|
fmt.Fprintf(&out, "%s%s %s %s %s%s\n",
|
|
prefix, joint, shortBranchID(b.ID), branchTitle(b, depth), turnText(b.Turns), current)
|
|
for i, child := range children[b.ID] {
|
|
walk(child, childPrefix, i == len(children[b.ID])-1, depth+1)
|
|
}
|
|
}
|
|
for i, root := range roots {
|
|
walk(root, "", i == len(roots)-1, 0)
|
|
}
|
|
for _, b := range branches {
|
|
walk(b, "", true, 0)
|
|
}
|
|
return strings.TrimRight(out.String(), "\n")
|
|
}
|
|
|
|
func branchTitle(b agent.BranchInfo, depth int) string {
|
|
title := strings.TrimSpace(b.Name)
|
|
if title == "" {
|
|
title = strings.TrimSpace(b.Preview)
|
|
}
|
|
if label, ok := structuredBranchLabel(title); ok {
|
|
return label
|
|
}
|
|
maxRunes := 32 - depth*4
|
|
if maxRunes < 18 {
|
|
maxRunes = 18
|
|
}
|
|
title = oneLineBranch(title, maxRunes)
|
|
if title == "" {
|
|
return "(untitled)"
|
|
}
|
|
return title
|
|
}
|
|
|
|
func structuredBranchLabel(s string) (string, bool) {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return "", false
|
|
}
|
|
switch s[0] {
|
|
case '{':
|
|
lower := strings.ToLower(s)
|
|
switch {
|
|
case strings.Contains(lower, `"msg"`) && strings.Contains(lower, "success"):
|
|
return "JSON response: success", true
|
|
case strings.Contains(lower, `"error"`) || strings.Contains(lower, `"errors"`):
|
|
return "JSON payload: error", true
|
|
default:
|
|
return "JSON object", true
|
|
}
|
|
case '[':
|
|
return "JSON array", true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
func turnText(n int) string {
|
|
if n == 1 {
|
|
return "1 turn"
|
|
}
|
|
return fmt.Sprintf("%d turns", n)
|
|
}
|
|
|
|
func shortBranchID(id string) string {
|
|
if len(id) >= 16 && numeric(id[:8]) && id[8] == '-' && numeric(id[9:15]) && id[15] == '.' {
|
|
fracEnd := 16
|
|
for fracEnd < len(id) && fracEnd < 19 && id[fracEnd] >= '0' && id[fracEnd] <= '9' {
|
|
fracEnd++
|
|
}
|
|
if fracEnd > 16 {
|
|
return id[4:8] + "-" + id[9:15] + "." + id[16:fracEnd]
|
|
}
|
|
return id[4:8] + "-" + id[9:15]
|
|
}
|
|
return oneLineBranch(id, 18)
|
|
}
|
|
|
|
func numeric(s string) bool {
|
|
for _, ch := range s {
|
|
if ch < '0' || ch > '9' {
|
|
return false
|
|
}
|
|
}
|
|
return s != ""
|
|
}
|
|
|
|
func oneLineBranch(s string, maxRunes int) string {
|
|
s = strings.Join(strings.Fields(s), " ")
|
|
if maxRunes <= 0 {
|
|
return s
|
|
}
|
|
r := []rune(s)
|
|
if len(r) <= maxRunes {
|
|
return s
|
|
}
|
|
if maxRunes <= 1 {
|
|
return string(r[:maxRunes])
|
|
}
|
|
return string(r[:maxRunes-1]) + "..."
|
|
}
|