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}