chat.go

  1package chat
  2
  3import (
  4	"context"
  5
  6	"github.com/charmbracelet/bubbles/v2/key"
  7	tea "github.com/charmbracelet/bubbletea/v2"
  8	"github.com/charmbracelet/crush/internal/app"
  9	"github.com/charmbracelet/crush/internal/logging"
 10	"github.com/charmbracelet/crush/internal/message"
 11	"github.com/charmbracelet/crush/internal/session"
 12	"github.com/charmbracelet/crush/internal/tui/components/chat"
 13	"github.com/charmbracelet/crush/internal/tui/components/chat/editor"
 14	"github.com/charmbracelet/crush/internal/tui/components/chat/sidebar"
 15	"github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
 16	"github.com/charmbracelet/crush/internal/tui/layout"
 17	"github.com/charmbracelet/crush/internal/tui/page"
 18	"github.com/charmbracelet/crush/internal/tui/util"
 19)
 20
 21var ChatPage page.PageID = "chat"
 22
 23type ChatFocusedMsg struct {
 24	Focused bool // True if the chat input is focused, false otherwise
 25}
 26
 27type (
 28	OpenFilePickerMsg struct{}
 29	chatPage          struct {
 30		app *app.App
 31
 32		layout layout.SplitPaneLayout
 33
 34		session session.Session
 35
 36		keyMap KeyMap
 37
 38		chatFocused bool
 39	}
 40)
 41
 42func (p *chatPage) Init() tea.Cmd {
 43	cmd := p.layout.Init()
 44	return tea.Batch(
 45		cmd,
 46		p.layout.FocusPanel(layout.BottomPanel), // Focus on the bottom panel (editor)
 47	)
 48}
 49
 50func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 51	var cmds []tea.Cmd
 52	switch msg := msg.(type) {
 53	case tea.WindowSizeMsg:
 54		cmd := p.layout.SetSize(msg.Width, msg.Height)
 55		cmds = append(cmds, cmd)
 56	case chat.SendMsg:
 57		cmd := p.sendMessage(msg.Text, msg.Attachments)
 58		if cmd != nil {
 59			return p, cmd
 60		}
 61	case commands.CommandRunCustomMsg:
 62		// Check if the agent is busy before executing custom commands
 63		if p.app.CoderAgent.IsBusy() {
 64			return p, util.ReportWarn("Agent is busy, please wait before executing a command...")
 65		}
 66
 67		// Handle custom command execution
 68		cmd := p.sendMessage(msg.Content, nil)
 69		if cmd != nil {
 70			return p, cmd
 71		}
 72	case chat.SessionSelectedMsg:
 73		if p.session.ID == "" {
 74			cmd := p.setMessages()
 75			if cmd != nil {
 76				cmds = append(cmds, cmd)
 77			}
 78		}
 79		p.session = msg
 80	case tea.KeyPressMsg:
 81		switch {
 82		case key.Matches(msg, p.keyMap.NewSession):
 83			p.session = session.Session{}
 84			return p, tea.Batch(
 85				p.clearMessages(),
 86				util.CmdHandler(chat.SessionClearedMsg{}),
 87			)
 88
 89		case key.Matches(msg, p.keyMap.FilePicker):
 90			return p, util.CmdHandler(OpenFilePickerMsg{})
 91		case key.Matches(msg, p.keyMap.Tab):
 92			logging.Info("Tab key pressed, toggling chat focus")
 93			if p.session.ID == "" {
 94				return p, nil
 95			}
 96			p.chatFocused = !p.chatFocused
 97			if p.chatFocused {
 98				cmds = append(cmds, p.layout.FocusPanel(layout.LeftPanel))
 99				cmds = append(cmds, util.CmdHandler(ChatFocusedMsg{Focused: true}))
100			} else {
101				cmds = append(cmds, p.layout.FocusPanel(layout.BottomPanel))
102				cmds = append(cmds, util.CmdHandler(ChatFocusedMsg{Focused: false}))
103			}
104			return p, tea.Batch(cmds...)
105		case key.Matches(msg, p.keyMap.Cancel):
106			if p.session.ID != "" {
107				// Cancel the current session's generation process
108				// This allows users to interrupt long-running operations
109				p.app.CoderAgent.Cancel(p.session.ID)
110				return p, nil
111			}
112		}
113	}
114	u, cmd := p.layout.Update(msg)
115	cmds = append(cmds, cmd)
116	p.layout = u.(layout.SplitPaneLayout)
117
118	return p, tea.Batch(cmds...)
119}
120
121func (p *chatPage) setMessages() tea.Cmd {
122	messagesContainer := layout.NewContainer(
123		chat.NewMessagesListCmp(p.app),
124		layout.WithPadding(1, 1, 0, 1),
125	)
126	return tea.Batch(p.layout.SetLeftPanel(messagesContainer), messagesContainer.Init())
127}
128
129func (p *chatPage) clearMessages() tea.Cmd {
130	return p.layout.ClearLeftPanel()
131}
132
133func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
134	var cmds []tea.Cmd
135	if p.session.ID == "" {
136		session, err := p.app.Sessions.Create(context.Background(), "New Session")
137		if err != nil {
138			return util.ReportError(err)
139		}
140
141		p.session = session
142		cmd := p.setMessages()
143		if cmd != nil {
144			cmds = append(cmds, cmd)
145		}
146		cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
147	}
148
149	_, err := p.app.CoderAgent.Run(context.Background(), p.session.ID, text, attachments...)
150	if err != nil {
151		return util.ReportError(err)
152	}
153	return tea.Batch(cmds...)
154}
155
156func (p *chatPage) SetSize(width, height int) tea.Cmd {
157	return p.layout.SetSize(width, height)
158}
159
160func (p *chatPage) GetSize() (int, int) {
161	return p.layout.GetSize()
162}
163
164func (p *chatPage) View() tea.View {
165	return p.layout.View()
166}
167
168func NewChatPage(app *app.App) util.Model {
169	sidebarContainer := layout.NewContainer(
170		sidebar.NewSidebarCmp(),
171		layout.WithPadding(1, 1, 1, 1),
172	)
173	editorContainer := layout.NewContainer(
174		editor.NewEditorCmp(app),
175	)
176	return &chatPage{
177		app: app,
178		layout: layout.NewSplitPane(
179			layout.WithRightPanel(sidebarContainer),
180			layout.WithBottomPanel(editorContainer),
181			layout.WithFixedBottomHeight(5),
182			layout.WithFixedRightWidth(31),
183		),
184		keyMap: DefaultKeyMap(),
185	}
186}