| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- package compact
- import (
- "context"
- "github.com/charmbracelet/bubbles/v2/key"
- tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/lipgloss/v2"
- "github.com/charmbracelet/crush/internal/llm/agent"
- "github.com/charmbracelet/crush/internal/tui/components/core"
- "github.com/charmbracelet/crush/internal/tui/components/dialogs"
- "github.com/charmbracelet/crush/internal/tui/styles"
- "github.com/charmbracelet/crush/internal/tui/util"
- )
- const CompactDialogID dialogs.DialogID = "compact"
- // CompactDialog interface for the session compact dialog
- type CompactDialog interface {
- dialogs.DialogModel
- }
- type compactDialogCmp struct {
- wWidth, wHeight int
- width, height int
- selected int
- keyMap KeyMap
- sessionID string
- state compactState
- progress string
- agent agent.Service
- noAsk bool // If true, skip confirmation dialog
- }
- type compactState int
- const (
- stateConfirm compactState = iota
- stateCompacting
- stateError
- )
- // NewCompactDialogCmp creates a new session compact dialog
- func NewCompactDialogCmp(agent agent.Service, sessionID string, noAsk bool) CompactDialog {
- return &compactDialogCmp{
- sessionID: sessionID,
- keyMap: DefaultKeyMap(),
- state: stateConfirm,
- selected: 0,
- agent: agent,
- noAsk: noAsk,
- }
- }
- func (c *compactDialogCmp) Init() tea.Cmd {
- if c.noAsk {
- // If noAsk is true, skip confirmation and start compaction immediately
- return c.startCompaction()
- }
- return nil
- }
- func (c *compactDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- switch msg := msg.(type) {
- case tea.WindowSizeMsg:
- c.wWidth = msg.Width
- c.wHeight = msg.Height
- cmd := c.SetSize()
- return c, cmd
- case tea.KeyPressMsg:
- switch c.state {
- case stateConfirm:
- switch {
- case key.Matches(msg, c.keyMap.ChangeSelection):
- c.selected = (c.selected + 1) % 2
- return c, nil
- case key.Matches(msg, c.keyMap.Select):
- if c.selected == 0 {
- return c, c.startCompaction()
- } else {
- return c, util.CmdHandler(dialogs.CloseDialogMsg{})
- }
- case key.Matches(msg, c.keyMap.Y):
- return c, c.startCompaction()
- case key.Matches(msg, c.keyMap.N):
- return c, util.CmdHandler(dialogs.CloseDialogMsg{})
- case key.Matches(msg, c.keyMap.Close):
- return c, util.CmdHandler(dialogs.CloseDialogMsg{})
- }
- case stateCompacting:
- switch {
- case key.Matches(msg, c.keyMap.Close):
- return c, util.CmdHandler(dialogs.CloseDialogMsg{})
- }
- case stateError:
- switch {
- case key.Matches(msg, c.keyMap.Select):
- return c, util.CmdHandler(dialogs.CloseDialogMsg{})
- case key.Matches(msg, c.keyMap.Close):
- return c, util.CmdHandler(dialogs.CloseDialogMsg{})
- }
- }
- case agent.AgentEvent:
- if msg.Type == agent.AgentEventTypeSummarize {
- if msg.Error != nil {
- c.state = stateError
- c.progress = "Error: " + msg.Error.Error()
- } else if msg.Done {
- return c, util.CmdHandler(
- dialogs.CloseDialogMsg{},
- )
- } else {
- c.progress = msg.Progress
- }
- }
- return c, nil
- }
- return c, nil
- }
- func (c *compactDialogCmp) startCompaction() tea.Cmd {
- c.state = stateCompacting
- c.progress = "Starting summarization..."
- return func() tea.Msg {
- err := c.agent.Summarize(context.Background(), c.sessionID)
- if err != nil {
- c.state = stateError
- c.progress = "Error: " + err.Error()
- }
- return nil
- }
- }
- func (c *compactDialogCmp) renderButtons() string {
- t := styles.CurrentTheme()
- baseStyle := t.S().Base
- buttons := []core.ButtonOpts{
- {
- Text: "Yes",
- UnderlineIndex: 0, // "Y"
- Selected: c.selected == 0,
- },
- {
- Text: "No",
- UnderlineIndex: 0, // "N"
- Selected: c.selected == 1,
- },
- }
- content := core.SelectableButtons(buttons, " ")
- return baseStyle.AlignHorizontal(lipgloss.Right).Width(c.width - 4).Render(content)
- }
- func (c *compactDialogCmp) renderContent() string {
- t := styles.CurrentTheme()
- baseStyle := t.S().Base
- switch c.state {
- case stateConfirm:
- explanation := t.S().Text.
- Width(c.width - 4).
- 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.")
- question := t.S().Text.
- Width(c.width - 4).
- Render("Do you want to continue?")
- return baseStyle.Render(lipgloss.JoinVertical(
- lipgloss.Left,
- explanation,
- "",
- question,
- ))
- case stateCompacting:
- return baseStyle.Render(lipgloss.JoinVertical(
- lipgloss.Left,
- c.progress,
- "",
- "Please wait...",
- ))
- case stateError:
- return baseStyle.Render(lipgloss.JoinVertical(
- lipgloss.Left,
- c.progress,
- "",
- "Press Enter to close",
- ))
- }
- return ""
- }
- func (c *compactDialogCmp) render() string {
- t := styles.CurrentTheme()
- baseStyle := t.S().Base
- var title string
- switch c.state {
- case stateConfirm:
- title = "Compact Session"
- case stateCompacting:
- title = "Compacting Session"
- case stateError:
- title = "Compact Failed"
- }
- titleView := core.Title(title, c.width-4)
- content := c.renderContent()
- var dialogContent string
- if c.state == stateConfirm {
- buttons := c.renderButtons()
- dialogContent = lipgloss.JoinVertical(
- lipgloss.Top,
- titleView,
- "",
- content,
- "",
- buttons,
- "",
- )
- } else {
- dialogContent = lipgloss.JoinVertical(
- lipgloss.Top,
- titleView,
- "",
- content,
- "",
- )
- }
- return baseStyle.
- Padding(0, 1).
- Border(lipgloss.RoundedBorder()).
- BorderForeground(t.BorderFocus).
- Width(c.width).
- Render(dialogContent)
- }
- func (c *compactDialogCmp) View() string {
- return c.render()
- }
- // SetSize sets the size of the component.
- func (c *compactDialogCmp) SetSize() tea.Cmd {
- c.width = min(90, c.wWidth)
- c.height = min(15, c.wHeight)
- return nil
- }
- func (c *compactDialogCmp) Position() (int, int) {
- row := (c.wHeight / 2) - (c.height / 2)
- col := (c.wWidth / 2) - (c.width / 2)
- return row, col
- }
- // ID implements CompactDialog.
- func (c *compactDialogCmp) ID() dialogs.DialogID {
- return CompactDialogID
- }
|