返回 DeepSeek-Reasonix
inbox_queue.go
根目录 / internal / cli / inbox_queue.go
1 package cli
2
3 import (
4 "fmt"
5 "strings"
6
7 "reasonix/internal/control"
8 "reasonix/internal/sessioninbox"
9 )
10
11 // inboxPreview is a bounded UI row for the composer shelf (never full body).
12 type inboxPreview struct {
13 ID string
14 Preview string
15 State sessioninbox.InboxState
16 Intent sessioninbox.InboxIntent
17 Pos int
18 }
19
20 func (m *chatTUI) inboxSnap() sessioninbox.InboxSnapshot {
21 if m == nil || m.ctrl == nil {
22 return sessioninbox.InboxSnapshot{}
23 }
24 return m.ctrl.InboxSnapshot()
25 }
26
27 func (m *chatTUI) inboxPreviews() []inboxPreview {
28 snap := m.inboxSnap()
29 out := make([]inboxPreview, 0, len(snap.Items))
30 for i, it := range snap.Items {
31 out = append(out, inboxPreview{
32 ID: it.ID,
33 Preview: it.Preview,
34 State: it.State,
35 Intent: it.Intent,
36 Pos: i + 1,
37 })
38 }
39 return out
40 }
41
42 func (m *chatTUI) inboxQueuedCount() int {
43 return len(m.inboxSnap().Items)
44 }
45
46 // enqueueFollowup persists a follow-up and kicks dispatch if the controller is
47 // already idle. Only clears the composer on success.
48 func (m *chatTUI) enqueueFollowup(display, submit string) (sessioninbox.InboxReceipt, error) {
49 if m.ctrl == nil {
50 return sessioninbox.InboxReceipt{}, sessioninbox.ErrClosed
51 }
52 if ensurer, ok := m.ctrl.(interface{ EnsureSessionPath() }); ok {
53 ensurer.EnsureSessionPath()
54 }
55 return m.ctrl.TryEnqueueFollowup(control.InboxRequest{
56 Intent: sessioninbox.IntentFollowup,
57 Display: display,
58 Raw: submit,
59 Submit: submit,
60 Source: "cli",
61 })
62 }
63
64 // enqueueSteer persists then attempts mid-turn steer.
65 func (m *chatTUI) enqueueSteer(display, submit string) (sessioninbox.InboxReceipt, error) {
66 if m.ctrl == nil {
67 return sessioninbox.InboxReceipt{}, sessioninbox.ErrClosed
68 }
69 if ensurer, ok := m.ctrl.(interface{ EnsureSessionPath() }); ok {
70 ensurer.EnsureSessionPath()
71 }
72 return m.ctrl.TryEnqueueAndSteer(control.InboxRequest{
73 Intent: sessioninbox.IntentSteer,
74 Display: display,
75 Raw: submit,
76 Submit: submit,
77 Source: "cli",
78 })
79 }
80
81 // seedInbox is a test helper to push durable queue rows.
82 func (m *chatTUI) seedInbox(texts ...string) {
83 for _, text := range texts {
84 _, _ = m.enqueueFollowup(text, text)
85 }
86 }
87
88 // inboxBodies returns full submit texts in queue order (tests only).
89 func (m *chatTUI) inboxBodies() []string {
90 snap := m.inboxSnap()
91 out := make([]string, 0, len(snap.Items))
92 for _, it := range snap.Items {
93 _, env, err := m.ctrl.ReadInboxItem(it.ID)
94 if err != nil {
95 out = append(out, it.Preview)
96 continue
97 }
98 out = append(out, env.SubmitText)
99 }
100 return out
101 }
102
103 // handleQueueSlash runs /queue and /steer as local commands even while running.
104 func (m *chatTUI) handleQueueSlash(line string) (handled bool, notice string) {
105 fields := strings.Fields(line)
106 if len(fields) == 0 {
107 return false, ""
108 }
109 cmd := strings.ToLower(fields[0])
110 args := fields[1:]
111 switch cmd {
112 case "/steer":
113 text := strings.TrimSpace(strings.TrimPrefix(line, fields[0]))
114 if text == "" {
115 return true, "usage: /steer <guidance>"
116 }
117 body := m.expandPastedBlocks(text)
118 rec, err := m.enqueueSteer(body, body)
119 if err != nil {
120 return true, "steer: " + err.Error()
121 }
122 switch rec.Disposition {
123 case sessioninbox.DispositionSteerAccepted:
124 return true, fmt.Sprintf("steer accepted #%s", shortID(rec.ItemID))
125 case sessioninbox.DispositionQueuedFollowup:
126 return true, fmt.Sprintf("steer rejected — queued as follow-up #%s", shortID(rec.ItemID))
127 default:
128 return true, fmt.Sprintf("queued #%s (%s)", shortID(rec.ItemID), rec.Disposition)
129 }
130 case "/queue":
131 return true, m.runQueueCommand(args)
132 default:
133 return false, ""
134 }
135 }
136
137 func (m *chatTUI) runQueueCommand(args []string) string {
138 if len(args) == 0 {
139 args = []string{"list"}
140 }
141 sub := strings.ToLower(args[0])
142 rest := args[1:]
143 switch sub {
144 case "list", "ls", "status":
145 return m.renderQueueList()
146 case "show":
147 id, err := m.resolveQueueRef(rest)
148 if err != nil {
149 return err.Error()
150 }
151 _, env, err := m.ctrl.ReadInboxItem(id)
152 if err != nil {
153 return "show: " + err.Error()
154 }
155 return env.SubmitText
156 case "edit":
157 return m.editQueueItem(rest)
158 case "delete", "rm", "del":
159 id, err := m.resolveQueueRef(rest)
160 if err != nil {
161 return err.Error()
162 }
163 if err := m.ctrl.DeleteInboxItem(id); err != nil {
164 return "delete: " + err.Error()
165 }
166 return "deleted #" + shortID(id)
167 case "move":
168 return m.moveQueueItem(rest)
169 case "pause":
170 if err := m.ctrl.SetInboxPaused(true); err != nil {
171 return "pause: " + err.Error()
172 }
173 return "inbox paused"
174 case "resume":
175 if err := m.ctrl.SetInboxPaused(false); err != nil {
176 return "resume: " + err.Error()
177 }
178 return "inbox resumed"
179 case "retry":
180 id, err := m.resolveQueueRef(rest)
181 if err != nil {
182 return err.Error()
183 }
184 if err := m.ctrl.RetryInboxItem(id); err != nil {
185 return "retry: " + err.Error()
186 }
187 return "retry queued #" + shortID(id)
188 case "refresh":
189 id, err := m.resolveQueueRef(rest)
190 if err != nil {
191 return err.Error()
192 }
193 if err := m.ctrl.RefreshInboxReferences(id); err != nil {
194 return "refresh: " + err.Error()
195 }
196 return "refs refreshed #" + shortID(id)
197 default:
198 return "usage: /queue list|show|edit|delete|move|pause|resume|retry|refresh"
199 }
200 }
201
202 func (m *chatTUI) renderQueueList() string {
203 snap := m.inboxSnap()
204 if len(snap.Items) == 0 {
205 status := "inbox empty"
206 if snap.Paused {
207 status += " (paused)"
208 }
209 return status
210 }
211 var b strings.Builder
212 fmt.Fprintf(&b, "inbox rev=%d items=%d", snap.Revision, len(snap.Items))
213 if snap.Paused {
214 b.WriteString(" paused")
215 }
216 if snap.Recovered {
217 fmt.Fprintf(&b, " recovered=%d", snap.RecoveredN)
218 }
219 b.WriteByte('\n')
220 limit := min(len(snap.Items), 20)
221 for i := range limit {
222 it := snap.Items[i]
223 fmt.Fprintf(&b, " %d. [%s/%s] %s #%s\n", i+1, it.Intent, it.State, it.Preview, shortID(it.ID))
224 }
225 if len(snap.Items) > limit {
226 fmt.Fprintf(&b, " … and %d more (use /queue show <n>)\n", len(snap.Items)-limit)
227 }
228 return strings.TrimRight(b.String(), "\n")
229 }
230
231 func (m *chatTUI) editQueueItem(args []string) string {
232 if len(args) < 2 {
233 return "usage: /queue edit <n|id> <text>"
234 }
235 id, err := m.resolveQueueRef(args[:1])
236 if err != nil {
237 return err.Error()
238 }
239 text := strings.Join(args[1:], " ")
240 if _, err := m.ctrl.UpdateInboxItem(id, text, text, text); err != nil {
241 return "edit: " + err.Error()
242 }
243 return "updated #" + shortID(id)
244 }
245
246 func (m *chatTUI) moveQueueItem(args []string) string {
247 if len(args) < 2 {
248 return "usage: /queue move <n|id> <to-index>"
249 }
250 id, err := m.resolveQueueRef(args[:1])
251 if err != nil {
252 return err.Error()
253 }
254 var to int
255 if _, err := fmt.Sscanf(args[1], "%d", &to); err != nil {
256 return "move: bad index"
257 }
258 if err := m.ctrl.MoveInboxItem(id, to-1); err != nil {
259 return "move: " + err.Error()
260 }
261 return "moved #" + shortID(id)
262 }
263
264 func (m *chatTUI) resolveQueueRef(args []string) (string, error) {
265 if len(args) == 0 {
266 return "", fmt.Errorf("missing item ref (index or id)")
267 }
268 ref := args[0]
269 snap := m.inboxSnap()
270 // Numeric 1-based index.
271 var n int
272 if _, err := fmt.Sscanf(ref, "%d", &n); err == nil && n >= 1 && n <= len(snap.Items) {
273 return snap.Items[n-1].ID, nil
274 }
275 // Full or short id prefix.
276 for _, it := range snap.Items {
277 if it.ID == ref || strings.HasPrefix(it.ID, ref) {
278 return it.ID, nil
279 }
280 }
281 return "", fmt.Errorf("unknown inbox item %q", ref)
282 }
283
284 func shortID(id string) string {
285 if len(id) <= 8 {
286 return id
287 }
288 return id[:8]
289 }
290
291 // handleQueueReorder moves the selected item by delta (-1 up, +1 down).
292 func (m *chatTUI) handleQueueReorder(delta int) bool {
293 if m.queueEditCursor < 0 || m.inboxSelectedID == "" || m.ctrl == nil {
294 return false
295 }
296 to := m.queueEditCursor + delta
297 if to < 0 {
298 return false
299 }
300 if err := m.ctrl.MoveInboxItem(m.inboxSelectedID, to); err != nil {
301 m.notice("move: " + err.Error())
302 return true
303 }
304 m.queueEditCursor = to
305 return true
306 }
307
307 lines GO