返回 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, c.CurrentBranchID())
37 }
38
39 // CurrentBranchID is the tree id of the branch the controller is on: a head
40 // id inside a schema-2 log, or the file id on the main head and for schema-1
41 // sessions, whose branches are files.
42 func (c *Controller) CurrentBranchID() string {
43 if c.headBranchSession() != nil {
44 if ref, ok := c.SessionHead(); ok && ref.HeadID != "" && ref.HeadID != agent.SessionMainHead {
45 return ref.HeadID
46 }
47 }
48 return agent.BranchID(c.SessionPath())
49 }
50
51 func FormatBranchTree(branches []agent.BranchInfo, currentID string) string {
52 if len(branches) == 0 {
53 return "branches: none"
54 }
55 byID := map[string]agent.BranchInfo{}
56 children := map[string][]agent.BranchInfo{}
57 for _, b := range branches {
58 byID[b.ID] = b
59 }
60 var roots []agent.BranchInfo
61 for _, b := range branches {
62 if b.ParentID == "" {
63 roots = append(roots, b)
64 continue
65 }
66 if _, ok := byID[b.ParentID]; !ok {
67 roots = append(roots, b)
68 continue
69 }
70 children[b.ParentID] = append(children[b.ParentID], b)
71 }
72 var out strings.Builder
73 out.WriteString("branches:\n")
74 seen := map[string]bool{}
75 var walk func(agent.BranchInfo, string, bool, int)
76 walk = func(b agent.BranchInfo, prefix string, last bool, depth int) {
77 if seen[b.ID] {
78 return
79 }
80 seen[b.ID] = true
81 joint := "├─"
82 childPrefix := prefix + "│ "
83 if last {
84 joint = "└─"
85 childPrefix = prefix + " "
86 }
87 current := ""
88 if b.ID == currentID {
89 current = " current"
90 }
91 fmt.Fprintf(&out, "%s%s %s %s %s%s\n",
92 prefix, joint, shortBranchID(b.ID), branchTitle(b, depth), turnText(b.Turns), current)
93 for i, child := range children[b.ID] {
94 walk(child, childPrefix, i == len(children[b.ID])-1, depth+1)
95 }
96 }
97 for i, root := range roots {
98 walk(root, "", i == len(roots)-1, 0)
99 }
100 for _, b := range branches {
101 walk(b, "", true, 0)
102 }
103 return strings.TrimRight(out.String(), "\n")
104 }
105
106 func branchTitle(b agent.BranchInfo, depth int) string {
107 title := strings.TrimSpace(b.Name)
108 if title == "" {
109 title = strings.TrimSpace(b.Preview)
110 }
111 if label, ok := structuredBranchLabel(title); ok {
112 return label
113 }
114 maxRunes := max(32-depth*4, 18)
115 title = oneLineBranch(title, maxRunes)
116 if title == "" {
117 return "(untitled)"
118 }
119 return title
120 }
121
122 func structuredBranchLabel(s string) (string, bool) {
123 s = strings.TrimSpace(s)
124 if s == "" {
125 return "", false
126 }
127 switch s[0] {
128 case '{':
129 lower := strings.ToLower(s)
130 switch {
131 case strings.Contains(lower, `"msg"`) && strings.Contains(lower, "success"):
132 return "JSON response: success", true
133 case strings.Contains(lower, `"error"`) || strings.Contains(lower, `"errors"`):
134 return "JSON payload: error", true
135 default:
136 return "JSON object", true
137 }
138 case '[':
139 return "JSON array", true
140 default:
141 return "", false
142 }
143 }
144
145 func turnText(n int) string {
146 if n == 1 {
147 return "1 turn"
148 }
149 return fmt.Sprintf("%d turns", n)
150 }
151
152 func shortBranchID(id string) string {
153 if len(id) >= 16 && numeric(id[:8]) && id[8] == '-' && numeric(id[9:15]) && id[15] == '.' {
154 fracEnd := 16
155 for fracEnd < len(id) && fracEnd < 19 && id[fracEnd] >= '0' && id[fracEnd] <= '9' {
156 fracEnd++
157 }
158 if fracEnd > 16 {
159 return id[4:8] + "-" + id[9:15] + "." + id[16:fracEnd]
160 }
161 return id[4:8] + "-" + id[9:15]
162 }
163 return oneLineBranch(id, 18)
164 }
165
166 func numeric(s string) bool {
167 for _, ch := range s {
168 if ch < '0' || ch > '9' {
169 return false
170 }
171 }
172 return s != ""
173 }
174
175 func oneLineBranch(s string, maxRunes int) string {
176 s = strings.Join(strings.Fields(s), " ")
177 if maxRunes <= 0 {
178 return s
179 }
180 r := []rune(s)
181 if len(r) <= maxRunes {
182 return s
183 }
184 if maxRunes <= 1 {
185 return string(r[:maxRunes])
186 }
187 return string(r[:maxRunes-1]) + "..."
188 }
189
189 lines GO