Vectrum
Sign In
80 lines · 2.5 KB · Go
package tui

import (
	"context"
	"fmt"
	"time"

	tea "github.com/charmbracelet/bubbletea"
)

// compactDone carries a finished /compact back to the update loop. did is
// false when the agent found nothing worth folding, which is not an error and
// must not be reported as one.
type compactDone struct {
	did bool
	err error
}

// startCompact answers /compact. It is a model call with the whole older half
// of the conversation in it, so it gets the spinner and the interrupt key that
// a turn gets — the difference is that no prompt goes with it and no turn
// counter moves.
func (m *model) startCompact() tea.Cmd {
	ctx, cancel := context.WithCancel(m.parent)
	m.cancel = cancel
	m.compacting = true
	m.started = time.Now()
	m.closeMenu()

	a, save := m.opts.Agent, m.saveSession
	run := func() tea.Msg {
		did, err := a.Compact(ctx)
		cancel()
		if did && err == nil {
			// The folded history is what -resume should pick up; leaving the
			// pre-compaction transcript on disk would undo the work on the
			// next start.
			if saveErr := save(); saveErr != nil {
				err = fmt.Errorf("saving session: %w", saveErr)
			}
		}
		return compactDone{did: did, err: err}
	}
	return tea.Batch(m.sync(), m.spin.Tick, m.tag(run))
}

// finishCompact lands the result. The agent has already announced a successful
// fold through EventCompact, so the only things left to say are the two cases
// that produce no such event.
func (m *model) finishCompact(msg compactDone) tea.Cmd {
	m.compacting = false
	m.interrupting = false
	m.cancel = nil

	switch {
	case msg.err != nil:
		m.tr.addError("compaction failed: " + msg.err.Error())
	case !msg.did:
		m.tr.addNotice("nothing to compact yet — the conversation is still short enough to keep whole")
	default:
		// The context pill reports what the model read last turn. After a
		// manual compaction nothing re-measures it until the next turn, so
		// leaving it would park the pre-compaction figure on screen for as
		// long as the session stays idle. Hiding it says "unknown", which is
		// the truth; automatic compaction needs no equivalent because the
		// request that corrects it goes out immediately.
		m.ctxTokens = 0
	}
	return m.sync()
}

// compactActivity is the line above the prompt while a compaction runs.
func (m *model) compactActivity() string {
	detail := fmt.Sprintf("%ds · esc to interrupt", int(time.Since(m.started).Seconds()))
	verb := "Compacting"
	if m.interrupting {
		verb = "Interrupting"
	}
	return m.spin.View() + " " + styCoral.Render(verb+"…") + styDim.Render(" ("+detail+")")
}