compact.go 5.9 KB

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