@@ -0,0 +1,266 @@
1+package compact
2+
3+import (
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+
17+const CompactDialogID dialogs.DialogID = "compact"
18+
19+// CompactDialog interface for the session compact dialog
20+type CompactDialog interface {
21+ dialogs.DialogModel
22+}
23+
24+type 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+
36+type compactState int
37+
38+const (
39+ stateConfirm compactState = iota
40+ stateCompacting
41+ stateError
42+)
43+
44+// NewCompactDialogCmp creates a new session compact dialog
45+func 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+
56+func (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+
64+func (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+
125+func (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+
138+func (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+
160+func (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+
198+func (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+
245+func (c *compactDialogCmp) View() tea.View {
246+ return tea.NewView(c.render())
247+}
248+
249+// SetSize sets the size of the component.
250+func (c *compactDialogCmp) SetSize() tea.Cmd {
251+ c.width = min(90, c.wWidth)
252+ c.height = min(15, c.wHeight)
253+ return nil
254+}
255+
256+func (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.
263+func (c *compactDialogCmp) ID() dialogs.DialogID {
264+ return CompactDialogID
265+}
266+