| 1 | package cli |
| 2 | |
| 3 | import ( |
| 4 | tea "charm.land/bubbletea/v2" |
| 5 | "context" |
| 6 | "fmt" |
| 7 | "reasonix/internal/control" |
| 8 | "reasonix/internal/event" |
| 9 | "reasonix/internal/i18n" |
| 10 | "strings" |
| 11 | ) |
| 12 | |
| 13 | func (m chatTUI) maintenanceCancellable() bool { |
| 14 | if m.maintenance == nil || m.ctrl == nil { |
| 15 | return false |
| 16 | } |
| 17 | switch strings.ToLower(strings.TrimSpace(m.maintenance.Activity)) { |
| 18 | case "", "starting", "running": |
| 19 | return true |
| 20 | default: |
| 21 | return false |
| 22 | } |
| 23 | } |
| 24 | |
| 25 | func (m chatTUI) runningWorkingLine(cancelRequested, styled bool) string { |
| 26 | if m.state != tuiRunning { |
| 27 | if m.maintenance == nil { |
| 28 | return "" |
| 29 | } |
| 30 | var label string |
| 31 | switch strings.ToLower(strings.TrimSpace(m.maintenance.Activity)) { |
| 32 | case "cancelling": |
| 33 | label = i18n.M.CompactionStopping |
| 34 | case "finalizing": |
| 35 | label = i18n.M.CompactionSaving |
| 36 | case "recovery_required": |
| 37 | label = i18n.M.CompactionRecoveryRequired |
| 38 | default: |
| 39 | label = i18n.M.CompactionWorking |
| 40 | } |
| 41 | return fmt.Sprintf(" %s %s", m.spinner.View(), label) |
| 42 | } |
| 43 | if m.retryAttempt > 0 && !cancelRequested { |
| 44 | if line, ok := m.waitingRecoveryLine(); ok { |
| 45 | return line |
| 46 | } |
| 47 | |
| 48 | return fmt.Sprintf(" "+i18n.M.ChatStatusRetryingFmt, m.spinner.View(), m.retryAttempt, m.retryMax) |
| 49 | } |
| 50 | |
| 51 | var working string |
| 52 | if cancelRequested { |
| 53 | working = fmt.Sprintf(" "+i18n.M.ChatStatusCancellingFmt, m.spinner.View(), m.elapsed) |
| 54 | } else { |
| 55 | phaseLabel := m.readStatusLabel |
| 56 | if phaseLabel == "" { |
| 57 | phaseLabel = turnPhaseStatusLabel(m.turnPhase) |
| 58 | } |
| 59 | if phaseLabel != "" { |
| 60 | working = fmt.Sprintf(" %s %s · %ds", m.spinner.View(), phaseLabel, m.elapsed) |
| 61 | } else { |
| 62 | working = fmt.Sprintf(" "+i18n.M.ChatStatusThinkingFmt, m.spinner.View(), m.elapsed) |
| 63 | } |
| 64 | } |
| 65 | if m.turnTokens > 0 { |
| 66 | working += " · ↓" + shortTokens(m.turnTokens) |
| 67 | } |
| 68 | if n := m.inboxQueuedCount(); n > 0 { |
| 69 | var queued string |
| 70 | if n == 1 { |
| 71 | queued = " · ✎ 1 in inbox" |
| 72 | } else { |
| 73 | queued = fmt.Sprintf(" · ✎ %d in inbox", n) |
| 74 | } |
| 75 | if m.inboxSnap().Paused { |
| 76 | queued += " (paused)" |
| 77 | } |
| 78 | if styled { |
| 79 | working += dim(queued) |
| 80 | } else { |
| 81 | working += queued |
| 82 | } |
| 83 | } |
| 84 | return working |
| 85 | } |
| 86 | |
| 87 | func (m *chatTUI) runCompactCommand(input, typedCmd string) tea.Cmd { |
| 88 | m.echoLocalCommand(input) |
| 89 | // Register maintenance before returning so Esc and new input see its owner. |
| 90 | // The worker runs in the background; trailing text remains summary guidance. |
| 91 | if submitter, ok := m.ctrl.(interface { |
| 92 | SubmitDisplayWithResult(display, input string) control.SubmitResult |
| 93 | }); ok { |
| 94 | result := submitter.SubmitDisplayWithResult(input, input) |
| 95 | if result.OperationID != "" { |
| 96 | op := &event.SessionOperationInfo{ |
| 97 | OperationID: result.OperationID, |
| 98 | Kind: "compact", |
| 99 | Activity: "running", |
| 100 | Status: "running", |
| 101 | } |
| 102 | m.maintenance = op |
| 103 | m.renderSessionOperation(op) |
| 104 | } |
| 105 | return nil |
| 106 | } |
| 107 | |
| 108 | // Compatibility path for older SessionAPI implementations. Keep an |
| 109 | // explicit maintenance placeholder so Esc preserves the draft while the |
| 110 | // blocking Compact call runs as a Bubble Tea command. |
| 111 | focus := strings.TrimSpace(strings.TrimPrefix(input, typedCmd)) |
| 112 | m.maintenance = &event.SessionOperationInfo{Kind: "compact", Activity: "starting", Status: "running"} |
| 113 | m.compactCompatibilityPending = true |
| 114 | m.compactLifecycleObserved = false |
| 115 | return func() tea.Msg { return compactDoneMsg{err: m.ctrl.Compact(context.Background(), focus)} } |
| 116 | } |
| 117 | |
| 118 | func (m *chatTUI) stopMaintenance() { |
| 119 | m.ctrl.Cancel() |
| 120 | updated := *m.maintenance |
| 121 | updated.Activity = "cancelling" |
| 122 | updated.Status = "cancelling" |
| 123 | m.maintenance = &updated |
| 124 | m.renderSessionOperation(&updated) |
| 125 | } |
| 126 |