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() tea.View {
246 return tea.NewView(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}