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