compact.go

  1package compact
  2
  3import (
  4	"context"
  5
  6	"github.com/charmbracelet/bubbles/v2/key"
  7	tea "github.com/charmbracelet/bubbletea/v2"
  8	"github.com/charmbracelet/lipgloss/v2"
  9
 10	"github.com/charmbracelet/crush/internal/client"
 11	"github.com/charmbracelet/crush/internal/llm/agent"
 12	"github.com/charmbracelet/crush/internal/tui/components/core"
 13	"github.com/charmbracelet/crush/internal/tui/components/dialogs"
 14	"github.com/charmbracelet/crush/internal/tui/styles"
 15	"github.com/charmbracelet/crush/internal/tui/util"
 16)
 17
 18const CompactDialogID dialogs.DialogID = "compact"
 19
 20// CompactDialog interface for the session compact dialog
 21type CompactDialog interface {
 22	dialogs.DialogModel
 23}
 24
 25type compactDialogCmp struct {
 26	wWidth, wHeight int
 27	width, height   int
 28	selected        int
 29	keyMap          KeyMap
 30	sessionID       string
 31	state           compactState
 32	progress        string
 33	client          *client.Client
 34	noAsk           bool // If true, skip confirmation dialog
 35}
 36
 37type compactState int
 38
 39const (
 40	stateConfirm compactState = iota
 41	stateCompacting
 42	stateError
 43)
 44
 45// NewCompactDialogCmp creates a new session compact dialog
 46func NewCompactDialogCmp(c *client.Client, sessionID string, noAsk bool) CompactDialog {
 47	return &compactDialogCmp{
 48		sessionID: sessionID,
 49		keyMap:    DefaultKeyMap(),
 50		state:     stateConfirm,
 51		selected:  0,
 52		client:    c,
 53		noAsk:     noAsk,
 54	}
 55}
 56
 57func (c *compactDialogCmp) Init() tea.Cmd {
 58	if c.noAsk {
 59		// If noAsk is true, skip confirmation and start compaction immediately
 60		return c.startCompaction()
 61	}
 62	return nil
 63}
 64
 65func (c *compactDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 66	switch msg := msg.(type) {
 67	case tea.WindowSizeMsg:
 68		c.wWidth = msg.Width
 69		c.wHeight = msg.Height
 70		cmd := c.SetSize()
 71		return c, cmd
 72
 73	case tea.KeyPressMsg:
 74		switch c.state {
 75		case stateConfirm:
 76			switch {
 77			case key.Matches(msg, c.keyMap.ChangeSelection):
 78				c.selected = (c.selected + 1) % 2
 79				return c, nil
 80			case key.Matches(msg, c.keyMap.Select):
 81				if c.selected == 0 {
 82					return c, c.startCompaction()
 83				} else {
 84					return c, util.CmdHandler(dialogs.CloseDialogMsg{})
 85				}
 86			case key.Matches(msg, c.keyMap.Y):
 87				return c, c.startCompaction()
 88			case key.Matches(msg, c.keyMap.N):
 89				return c, util.CmdHandler(dialogs.CloseDialogMsg{})
 90			case key.Matches(msg, c.keyMap.Close):
 91				return c, util.CmdHandler(dialogs.CloseDialogMsg{})
 92			}
 93		case stateCompacting:
 94			switch {
 95			case key.Matches(msg, c.keyMap.Close):
 96				return c, util.CmdHandler(dialogs.CloseDialogMsg{})
 97			}
 98		case stateError:
 99			switch {
100			case key.Matches(msg, c.keyMap.Select):
101				return c, util.CmdHandler(dialogs.CloseDialogMsg{})
102			case key.Matches(msg, c.keyMap.Close):
103				return c, util.CmdHandler(dialogs.CloseDialogMsg{})
104			}
105		}
106
107	case agent.AgentEvent:
108		switch msg.Type {
109		case agent.AgentEventTypeSummarize:
110			if msg.Error != nil {
111				c.state = stateError
112				c.progress = "Error: " + msg.Error.Error()
113			} else if msg.Done {
114				return c, util.CmdHandler(dialogs.CloseDialogMsg{})
115			} else {
116				c.progress = msg.Progress
117			}
118		case agent.AgentEventTypeError:
119			// Handle errors that occur during summarization but are sent as separate error events.
120			c.state = stateError
121			if msg.Error != nil {
122				c.progress = "Error: " + msg.Error.Error()
123			} else {
124				c.progress = "An unknown error occurred"
125			}
126		}
127		return c, nil
128	}
129
130	return c, nil
131}
132
133func (c *compactDialogCmp) startCompaction() tea.Cmd {
134	c.state = stateCompacting
135	c.progress = "Starting summarization..."
136	return func() tea.Msg {
137		err := c.client.AgentSummarizeSession(context.Background(), c.sessionID)
138		if err != nil {
139			c.state = stateError
140			c.progress = "Error: " + err.Error()
141		}
142		return nil
143	}
144}
145
146func (c *compactDialogCmp) renderButtons() string {
147	t := styles.CurrentTheme()
148	baseStyle := t.S().Base
149
150	buttons := []core.ButtonOpts{
151		{
152			Text:           "Yes",
153			UnderlineIndex: 0, // "Y"
154			Selected:       c.selected == 0,
155		},
156		{
157			Text:           "No",
158			UnderlineIndex: 0, // "N"
159			Selected:       c.selected == 1,
160		},
161	}
162
163	content := core.SelectableButtons(buttons, "  ")
164
165	return baseStyle.AlignHorizontal(lipgloss.Right).Width(c.width - 4).Render(content)
166}
167
168func (c *compactDialogCmp) renderContent() string {
169	t := styles.CurrentTheme()
170	baseStyle := t.S().Base
171
172	switch c.state {
173	case stateConfirm:
174		explanation := t.S().Text.
175			Width(c.width - 4).
176			Render("This will summarize the current session and reset the context. The conversation history will be condensed into a summary to free up context space while preserving important information.")
177
178		question := t.S().Text.
179			Width(c.width - 4).
180			Render("Do you want to continue?")
181
182		return baseStyle.Render(lipgloss.JoinVertical(
183			lipgloss.Left,
184			explanation,
185			"",
186			question,
187		))
188	case stateCompacting:
189		return baseStyle.Render(lipgloss.JoinVertical(
190			lipgloss.Left,
191			c.progress,
192			"",
193			"Please wait...",
194		))
195	case stateError:
196		return baseStyle.Render(lipgloss.JoinVertical(
197			lipgloss.Left,
198			c.progress,
199			"",
200			"Press Enter to close",
201		))
202	}
203	return ""
204}
205
206func (c *compactDialogCmp) render() string {
207	t := styles.CurrentTheme()
208	baseStyle := t.S().Base
209
210	var title string
211	switch c.state {
212	case stateConfirm:
213		title = "Compact Session"
214	case stateCompacting:
215		title = "Compacting Session"
216	case stateError:
217		title = "Compact Failed"
218	}
219
220	titleView := core.Title(title, c.width-4)
221	content := c.renderContent()
222
223	var dialogContent string
224	if c.state == stateConfirm {
225		buttons := c.renderButtons()
226		dialogContent = lipgloss.JoinVertical(
227			lipgloss.Top,
228			titleView,
229			"",
230			content,
231			"",
232			buttons,
233			"",
234		)
235	} else {
236		dialogContent = lipgloss.JoinVertical(
237			lipgloss.Top,
238			titleView,
239			"",
240			content,
241			"",
242		)
243	}
244
245	return baseStyle.
246		Padding(0, 1).
247		Border(lipgloss.RoundedBorder()).
248		BorderForeground(t.BorderFocus).
249		Width(c.width).
250		Render(dialogContent)
251}
252
253func (c *compactDialogCmp) View() string {
254	return c.render()
255}
256
257// SetSize sets the size of the component.
258func (c *compactDialogCmp) SetSize() tea.Cmd {
259	c.width = min(90, c.wWidth)
260	c.height = min(15, c.wHeight)
261	return nil
262}
263
264func (c *compactDialogCmp) Position() (int, int) {
265	row := (c.wHeight / 2) - (c.height / 2)
266	col := (c.wWidth / 2) - (c.width / 2)
267	return row, col
268}
269
270// ID implements CompactDialog.
271func (c *compactDialogCmp) ID() dialogs.DialogID {
272	return CompactDialogID
273}