返回 DeepSeek-Reasonix
branches.go
根目录 / internal / control / branches.go
1 package control
2
3 import (
4 "fmt"
5 "strconv"
6 "strings"
7
8 "reasonix/internal/agent"
9 )
10
11 // ParseBranchTarget parses the arguments after "/branch". A leading positive
12 // integer means "branch from displayed turn N"; otherwise the whole argument is
13 // the optional branch name for a tip branch.
14 func ParseBranchTarget(args string) (turn int, name string, fromTurn bool, err error) {
15 args = strings.TrimSpace(args)
16 fields := strings.Fields(args)
17 if len(fields) == 0 {
18 return 0, "", false, nil
19 }
20 n, convErr := strconv.Atoi(fields[0])
21 if convErr != nil {
22 return 0, args, false, nil
23 }
24 if n <= 0 {
25 return 0, "", false, fmt.Errorf("usage: /branch [turn] [name]")
26 }
27 name = strings.TrimSpace(strings.TrimPrefix(args, fields[0]))
28 return n, name, true, nil
29 }
30
31 func (c *Controller) BranchTreeText() string {
32 branches, err := c.Branches()
33 if err != nil {
34 return "branches: " + err.Error()
35 }
36 return FormatBranchTree(branches, agent.BranchID(c.SessionPath()))
37 }
38
39 func FormatBranchTree(branches []agent.BranchInfo, currentID string) string {
40 if len(branches) == 0 {
41 return "branches: none"
42 }
43 byID := map[string]agent.BranchInfo{}
44 children := map[string][]agent.BranchInfo{}
45 for _, b := range branches {
46 byID[b.ID] = b
47 }
48 var roots []agent.BranchInfo
49 for _, b := range branches {
50 if b.ParentID == "" {
51 roots = append(roots, b)
52 continue
53 }
54 if _, ok := byID[b.ParentID]; !ok {
55 roots = append(roots, b)
56 continue
57 }
58 children[b.ParentID] = append(children[b.ParentID], b)
59 }
60 var out strings.Builder
61 out.WriteString("branches:\n")
62 seen := map[string]bool{}
63 var walk func(agent.BranchInfo, string, bool, int)
64 walk = func(b agent.BranchInfo, prefix string, last bool, depth int) {
65 if seen[b.ID] {
66 return
67 }
68 seen[b.ID] = true
69 joint := "├─"
70 childPrefix := prefix + "│ "
71 if last {
72 joint = "└─"
73 childPrefix = prefix + " "
74 }
75 current := ""
76 if b.ID == currentID {
77 current = " current"
78 }
79 fmt.Fprintf(&out, "%s%s %s %s %s%s\n",
80 prefix, joint, shortBranchID(b.ID), branchTitle(b, depth), turnText(b.Turns), current)
81 for i, child := range children[b.ID] {
82 walk(child, childPrefix, i == len(children[b.ID])-1, depth+1)
83 }
84 }
85 for i, root := range roots {
86 walk(root, "", i == len(roots)-1, 0)
87 }
88 for _, b := range branches {
89 walk(b, "", true, 0)
90 }
91 return strings.TrimRight(out.String(), "\n")
92 }
93
94 func branchTitle(b agent.BranchInfo, depth int) string {
95 title := strings.TrimSpace(b.Name)
96 if title == "" {
97 title = strings.TrimSpace(b.Preview)
98 }
99 if label, ok := structuredBranchLabel(title); ok {
100 return label
101 }
102 maxRunes := 32 - depth*4
103 if maxRunes < 18 {
104 maxRunes = 18
105 }
106 title = oneLineBranch(title, maxRunes)
107 if title == "" {
108 return "(untitled)"
109 }
110 return title
111 }
112
113 func structuredBranchLabel(s string) (string, bool) {
114 s = strings.TrimSpace(s)
115 if s == "" {
116 return "", false
117 }
118 switch s[0] {
119 case '{':
120 lower := strings.ToLower(s)
121 switch {
122 case strings.Contains(lower, `"msg"`) && strings.Contains(lower, "success"):
123 return "JSON response: success", true
124 case strings.Contains(lower, `"error"`) || strings.Contains(lower, `"errors"`):
125 return "JSON payload: error", true
126 default:
127 return "JSON object", true
128 }
129 case '[':
130 return "JSON array", true
131 default:
132 return "", false
133 }
134 }
135
136 func turnText(n int) string {
137 if n == 1 {
138 return "1 turn"
139 }
140 return fmt.Sprintf("%d turns", n)
141 }
142
143 func shortBranchID(id string) string {
144 if len(id) >= 16 && numeric(id[:8]) && id[8] == '-' && numeric(id[9:15]) && id[15] == '.' {
145 fracEnd := 16
146 for fracEnd < len(id) && fracEnd < 19 && id[fracEnd] >= '0' && id[fracEnd] <= '9' {
147 fracEnd++
148 }
149 if fracEnd > 16 {
150 return id[4:8] + "-" + id[9:15] + "." + id[16:fracEnd]
151 }
152 return id[4:8] + "-" + id[9:15]
153 }
154 return oneLineBranch(id, 18)
155 }
156
157 func numeric(s string) bool {
158 for _, ch := range s {
159 if ch < '0' || ch > '9' {
160 return false
161 }
162 }
163 return s != ""
164 }
165
166 func oneLineBranch(s string, maxRunes int) string {
167 s = strings.Join(strings.Fields(s), " ")
168 if maxRunes <= 0 {
169 return s
170 }
171 r := []rune(s)
172 if len(r) <= maxRunes {
173 return s
174 }
175 if maxRunes <= 1 {
176 return string(r[:maxRunes])
177 }
178 return string(r[:maxRunes-1]) + "..."
179 }
180
180 lines GO