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}