@@ -2,21 +2,27 @@ package chat
22
33import (
44 "context"
5− "strings"
65 "time"
76
87 "github.com/charmbracelet/bubbles/v2/key"
8+ "github.com/charmbracelet/bubbles/v2/spinner"
99 tea "github.com/charmbracelet/bubbletea/v2"
1010 "github.com/charmbracelet/crush/internal/app"
1111 "github.com/charmbracelet/crush/internal/config"
12+ "github.com/charmbracelet/crush/internal/history"
1213 "github.com/charmbracelet/crush/internal/message"
14+ "github.com/charmbracelet/crush/internal/pubsub"
1315 "github.com/charmbracelet/crush/internal/session"
16+ "github.com/charmbracelet/crush/internal/tui/components/anim"
1417 "github.com/charmbracelet/crush/internal/tui/components/chat"
1518 "github.com/charmbracelet/crush/internal/tui/components/chat/editor"
1619 "github.com/charmbracelet/crush/internal/tui/components/chat/header"
1720 "github.com/charmbracelet/crush/internal/tui/components/chat/sidebar"
21+ "github.com/charmbracelet/crush/internal/tui/components/chat/splash"
22+ "github.com/charmbracelet/crush/internal/tui/components/completions"
1823 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
1924 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
25+ "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
2026 "github.com/charmbracelet/crush/internal/tui/page"
2127 "github.com/charmbracelet/crush/internal/tui/styles"
2228 "github.com/charmbracelet/crush/internal/tui/util"
@@ -26,8 +32,6 @@ import (
2632
2733var ChatPageID page.PageID = "chat"
2834
29−const CompactModeBreakpoint = 120 // Width at which the chat page switches to compact mode
30−
3135type (
3236 OpenFilePickerMsg struct{}
3337 ChatFocusedMsg struct {
@@ -36,104 +40,171 @@ type (
3640 CancelTimerExpiredMsg struct{}
3741)
3842
43+type ChatState string
44+
45+const (
46+ ChatStateOnboarding ChatState = "onboarding"
47+ ChatStateInitProject ChatState = "init_project"
48+ ChatStateNewMessage ChatState = "new_message"
49+ ChatStateInSession ChatState = "in_session"
50+)
51+
52+type PanelType string
53+
54+const (
55+ PanelTypeChat PanelType = "chat"
56+ PanelTypeEditor PanelType = "editor"
57+ PanelTypeSplash PanelType = "splash"
58+)
59+
60+const (
61+ CompactModeBreakpoint = 120 // Width at which the chat page switches to compact mode
62+ EditorHeight = 5 // Height of the editor input area including padding
63+ SideBarWidth = 31 // Width of the sidebar
64+ SideBarDetailsPadding = 1 // Padding for the sidebar details section
65+ HeaderHeight = 1 // Height of the header
66+)
67+
3968type ChatPage interface {
4069 util.Model
4170 layout.Help
4271}
4372
44−type chatPage struct {
45− wWidth, wHeight int // Window dimensions
46− app *app.App
47−
48− layout layout.SplitPaneLayout
49−
50− session session.Session
73+// cancelTimerCmd creates a command that expires the cancel timer after 2 seconds
74+func cancelTimerCmd() tea.Cmd {
75+ return tea.Tick(2*time.Second, func(time.Time) tea.Msg {
76+ return CancelTimerExpiredMsg{}
77+ })
78+}
5179
52− keyMap KeyMap
80+type chatPage struct {
81+ width, height int
82+ detailsWidth, detailsHeight int
83+ app *app.App
84+ state ChatState
85+ session session.Session
86+ keyMap KeyMap
87+ focusedPane PanelType
88+ // Compact mode
89+ compact bool
90+ header header.Header
91+ showingDetails bool
92+
93+ sidebar sidebar.Sidebar
94+ chat chat.MessageListCmp
95+ editor editor.Editor
96+ splash splash.Splash
97+ canceling bool
98+
99+ // This will force the compact mode even in big screens
100+ // usually triggered by the user command
101+ // this will also be set when the user config is set to compact mode
102+ forceCompact bool
103+}
53104
54− chatFocused bool
105+func New(app *app.App) ChatPage {
106+ return &chatPage{
107+ app: app,
108+ state: ChatStateOnboarding,
55109
56− compactMode bool
57− forceCompactMode bool // Force compact mode regardless of window size
58− showDetails bool // Show details in the header
59− header header.Header
60− compactSidebar layout.Container
110+ keyMap: DefaultKeyMap(),
61111
62− cancelPending bool // True if ESC was pressed once and waiting for second press
112+ header: header.New(app.LSPClients),
113+ sidebar: sidebar.New(app.History, app.LSPClients, false),
114+ chat: chat.New(app),
115+ editor: editor.New(app),
116+ splash: splash.New(),
117+ }
63118}
64119
65120func (p *chatPage) Init() tea.Cmd {
121+ cfg := config.Get()
122+ if cfg.IsReady() {
123+ if b, _ := config.ProjectNeedsInitialization(); b {
124+ p.state = ChatStateInitProject
125+ } else {
126+ p.state = ChatStateNewMessage
127+ p.focusedPane = PanelTypeEditor
128+ }
129+ }
130+
131+ compact := cfg.Options.TUI.CompactMode
132+ p.compact = compact
133+ p.forceCompact = compact
134+ p.sidebar.SetCompactMode(p.compact)
66135 return tea.Batch(
67− p.layout.Init(),
68− p.compactSidebar.Init(),
69− p.layout.FocusPanel(layout.BottomPanel), // Focus on the bottom panel (editor),
136+ p.header.Init(),
137+ p.sidebar.Init(),
138+ p.chat.Init(),
139+ p.editor.Init(),
140+ p.splash.Init(),
70141 )
71142}
72143
73−// cancelTimerCmd creates a command that expires the cancel timer after 2 seconds
74−func (p *chatPage) cancelTimerCmd() tea.Cmd {
75− return tea.Tick(2*time.Second, func(time.Time) tea.Msg {
76− return CancelTimerExpiredMsg{}
77− })
78−}
79−
80144func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
81145 var cmds []tea.Cmd
82146 switch msg := msg.(type) {
147+ case tea.WindowSizeMsg:
148+ return p, p.SetSize(msg.Width, msg.Height)
83149 case CancelTimerExpiredMsg:
84− p.cancelPending = false
150+ p.canceling = false
85151 return p, nil
86− case tea.WindowSizeMsg:
87− h, cmd := p.header.Update(msg)
88− cmds = append(cmds, cmd)
89− p.header = h.(header.Header)
90− cmds = append(cmds, p.compactSidebar.SetSize(msg.Width-4, 0))
91− // the mode is only relevant when there is a session
92− if p.session.ID != "" {
93− // Only auto-switch to compact mode if not forced
94− if !p.forceCompactMode {
95− if msg.Width <= CompactModeBreakpoint && p.wWidth > CompactModeBreakpoint {
96− p.wWidth = msg.Width
97− p.wHeight = msg.Height
98− cmds = append(cmds, p.setCompactMode(true))
99− return p, tea.Batch(cmds...)
100− } else if msg.Width > CompactModeBreakpoint && p.wWidth <= CompactModeBreakpoint {
101− p.wWidth = msg.Width
102− p.wHeight = msg.Height
103− return p, p.setCompactMode(false)
104− }
105− }
106− }
107− p.wWidth = msg.Width
108− p.wHeight = msg.Height
109− layoutHeight := msg.Height
110− if p.compactMode {
111− // make space for the header
112− layoutHeight -= 1
152+ case chat.SendMsg:
153+ return p, p.sendMessage(msg.Text, msg.Attachments)
154+ case chat.SessionSelectedMsg:
155+ return p, p.setSession(msg)
156+ case commands.ToggleCompactModeMsg:
157+ p.forceCompact = !p.forceCompact
158+ if p.forceCompact {
159+ p.setCompactMode(true)
160+ } else if p.width >= CompactModeBreakpoint {
161+ p.setCompactMode(false)
113162 }
114− cmd = p.layout.SetSize(msg.Width, layoutHeight)
163+ return p, p.SetSize(p.width, p.height)
164+ case pubsub.Event[session.Session]:
165+ // this needs to go to header/sidebar
166+ u, cmd := p.header.Update(msg)
167+ p.header = u.(header.Header)
168+ cmds = append(cmds, cmd)
169+ u, cmd = p.sidebar.Update(msg)
170+ p.sidebar = u.(sidebar.Sidebar)
171+ cmds = append(cmds, cmd)
172+ return p, tea.Batch(cmds...)
173+ case chat.SessionClearedMsg:
174+ u, cmd := p.header.Update(msg)
175+ p.header = u.(header.Header)
176+ cmds = append(cmds, cmd)
177+ u, cmd = p.sidebar.Update(msg)
178+ p.sidebar = u.(sidebar.Sidebar)
179+ cmds = append(cmds, cmd)
180+ u, cmd = p.chat.Update(msg)
181+ p.chat = u.(chat.MessageListCmp)
182+ cmds = append(cmds, cmd)
183+ return p, tea.Batch(cmds...)
184+ case filepicker.FilePickedMsg,
185+ completions.CompletionsClosedMsg,
186+ completions.SelectCompletionMsg:
187+ u, cmd := p.editor.Update(msg)
188+ p.editor = u.(editor.Editor)
189+ cmds = append(cmds, cmd)
190+ return p, tea.Batch(cmds...)
191+
192+ case pubsub.Event[message.Message],
193+ anim.StepMsg,
194+ spinner.TickMsg:
195+ // this needs to go to chat
196+ u, cmd := p.chat.Update(msg)
197+ p.chat = u.(chat.MessageListCmp)
198+ cmds = append(cmds, cmd)
199+ return p, tea.Batch(cmds...)
200+
201+ case pubsub.Event[history.File], sidebar.SessionFilesMsg:
202+ // this needs to go to sidebar
203+ u, cmd := p.sidebar.Update(msg)
204+ p.sidebar = u.(sidebar.Sidebar)
115205 cmds = append(cmds, cmd)
116206 return p, tea.Batch(cmds...)
117207
118− case chat.SendMsg:
119− cmd := p.sendMessage(msg.Text, msg.Attachments)
120− if cmd != nil {
121− return p, cmd
122− }
123− case commands.ToggleCompactModeMsg:
124− // Only allow toggling if window width is larger than compact breakpoint
125− if p.wWidth > CompactModeBreakpoint {
126− p.forceCompactMode = !p.forceCompactMode
127− // If force compact mode is enabled, switch to compact mode
128− // If force compact mode is disabled, switch based on window size
129− if p.forceCompactMode {
130− return p, p.setCompactMode(true)
131− } else {
132− // Return to auto mode based on window size
133− shouldBeCompact := p.wWidth <= CompactModeBreakpoint
134− return p, p.setCompactMode(shouldBeCompact)
135− }
136− }
137208 case commands.CommandRunCustomMsg:
138209 // Check if the agent is busy before executing custom commands
139210 if p.app.CoderAgent.IsBusy() {
@@ -145,30 +216,10 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
145216 if cmd != nil {
146217 return p, cmd
147218 }
148− case chat.SessionSelectedMsg:
149− if p.session.ID == "" {
150− cmd := p.setMessages()
151− if cmd != nil {
152− cmds = append(cmds, cmd)
153− }
154− }
155− needsModeChange := p.session.ID == ""
156− p.session = msg
157− p.header.SetSession(msg)
158− if needsModeChange && (p.wWidth <= CompactModeBreakpoint || p.forceCompactMode) {
159− cmds = append(cmds, p.setCompactMode(true))
160− }
161219 case tea.KeyPressMsg:
162220 switch {
163221 case key.Matches(msg, p.keyMap.NewSession):
164− p.session = session.Session{}
165− return p, tea.Batch(
166− p.clearMessages(),
167− util.CmdHandler(chat.SessionClearedMsg{}),
168− p.setCompactMode(false),
169− p.layout.FocusPanel(layout.BottomPanel),
170− util.CmdHandler(ChatFocusedMsg{Focused: false}),
171− )
222+ return p, p.newSession()
172223 case key.Matches(msg, p.keyMap.AddAttachment):
173224 agentCfg := config.Get().Agents["coder"]
174225 model := config.Get().GetModelByType(agentCfg.Model)
@@ -178,167 +229,264 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
178229 return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Name)
179230 }
180231 case key.Matches(msg, p.keyMap.Tab):
181− if p.session.ID == "" {
182− return p, nil
183− }
184− p.chatFocused = !p.chatFocused
185− if p.chatFocused {
186− cmds = append(cmds, p.layout.FocusPanel(layout.LeftPanel))
187− cmds = append(cmds, util.CmdHandler(ChatFocusedMsg{Focused: true}))
188− } else {
189− cmds = append(cmds, p.layout.FocusPanel(layout.BottomPanel))
190− cmds = append(cmds, util.CmdHandler(ChatFocusedMsg{Focused: false}))
191− }
192− return p, tea.Batch(cmds...)
232+ p.changeFocus()
233+ return p, nil
193234 case key.Matches(msg, p.keyMap.Cancel):
194− if p.session.ID != "" {
195− if p.cancelPending {
196− // Second ESC press - actually cancel the session
197− p.cancelPending = false
198− p.app.CoderAgent.Cancel(p.session.ID)
199− return p, nil
200− } else {
201− // First ESC press - start the timer
202− p.cancelPending = true
203− return p, p.cancelTimerCmd()
204− }
205− }
235+ return p, p.cancel()
206236 case key.Matches(msg, p.keyMap.Details):
207− if p.session.ID == "" || !p.compactMode {
208− return p, nil // No session to show details for
209− }
210− p.showDetails = !p.showDetails
211− p.header.SetDetailsOpen(p.showDetails)
212− if p.showDetails {
213− return p, tea.Batch()
214− }
215−
237+ p.showDetails()
216238 return p, nil
217239 }
240+
241+ // Send the key press to the focused pane
242+ switch p.focusedPane {
243+ case PanelTypeChat:
244+ u, cmd := p.chat.Update(msg)
245+ p.chat = u.(chat.MessageListCmp)
246+ cmds = append(cmds, cmd)
247+ case PanelTypeEditor:
248+ u, cmd := p.editor.Update(msg)
249+ p.editor = u.(editor.Editor)
250+ cmds = append(cmds, cmd)
251+ }
218252 }
219− u, cmd := p.layout.Update(msg)
220− cmds = append(cmds, cmd)
221− p.layout = u.(layout.SplitPaneLayout)
222− h, cmd := p.header.Update(msg)
223− p.header = h.(header.Header)
224− cmds = append(cmds, cmd)
225− s, cmd := p.compactSidebar.Update(msg)
226− p.compactSidebar = s.(layout.Container)
227− cmds = append(cmds, cmd)
228253 return p, tea.Batch(cmds...)
229254}
230255
231−func (p *chatPage) setMessages() tea.Cmd {
232− messagesContainer := layout.NewContainer(
233− chat.NewMessagesListCmp(p.app),
234− layout.WithPadding(1, 1, 0, 1),
235− )
236− return tea.Batch(p.layout.SetLeftPanel(messagesContainer), messagesContainer.Init())
237−}
256+func (p *chatPage) View() tea.View {
257+ var chatView tea.View
258+ t := styles.CurrentTheme()
259+ switch p.state {
260+ case ChatStateOnboarding, ChatStateInitProject:
261+ chatView = tea.NewView(
262+ t.S().Base.Render(
263+ p.splash.View().String(),
264+ ),
265+ )
266+ case ChatStateNewMessage:
267+ editorView := p.editor.View()
268+ chatView = tea.NewView(
269+ lipgloss.JoinVertical(
270+ lipgloss.Left,
271+ t.S().Base.Render(
272+ p.splash.View().String(),
273+ ),
274+ editorView.String(),
275+ ),
276+ )
277+ chatView.SetCursor(editorView.Cursor())
278+ case ChatStateInSession:
279+ messagesView := p.chat.View()
280+ editorView := p.editor.View()
281+ if p.compact {
282+ headerView := p.header.View()
283+ chatView = tea.NewView(
284+ lipgloss.JoinVertical(
285+ lipgloss.Left,
286+ headerView.String(),
287+ messagesView.String(),
288+ editorView.String(),
289+ ),
290+ )
291+ chatView.SetCursor(editorView.Cursor())
292+ } else {
293+ sidebarView := p.sidebar.View()
294+ messages := lipgloss.JoinHorizontal(
295+ lipgloss.Left,
296+ messagesView.String(),
297+ sidebarView.String(),
298+ )
299+ chatView = tea.NewView(
300+ lipgloss.JoinVertical(
301+ lipgloss.Left,
302+ messages,
303+ p.editor.View().String(),
304+ ),
305+ )
306+ chatView.SetCursor(editorView.Cursor())
307+ }
308+ default:
309+ chatView = tea.NewView("Unknown chat state")
310+ }
238311
239−func (p *chatPage) setSidebar() tea.Cmd {
240− sidebarContainer := sidebarCmp(p.app, false, p.session)
241− sidebarContainer.Init()
242− return p.layout.SetRightPanel(sidebarContainer)
243−}
312+ layers := []*lipgloss.Layer{
313+ lipgloss.NewLayer(chatView.String()).X(0).Y(0),
314+ }
244315
245−func (p *chatPage) clearMessages() tea.Cmd {
246− return p.layout.ClearLeftPanel()
316+ if p.showingDetails {
317+ style := t.S().Base.
318+ Width(p.detailsWidth).
319+ Border(lipgloss.RoundedBorder()).
320+ BorderForeground(t.BorderFocus)
321+ version := t.S().Subtle.Width(p.detailsWidth - 2).AlignHorizontal(lipgloss.Right).Render(version.Version)
322+ details := style.Render(
323+ lipgloss.JoinVertical(
324+ lipgloss.Left,
325+ p.sidebar.View().String(),
326+ version,
327+ ),
328+ )
329+ layers = append(layers, lipgloss.NewLayer(details).X(1).Y(1))
330+ }
331+ canvas := lipgloss.NewCanvas(
332+ layers...,
333+ )
334+ view := tea.NewView(canvas.Render())
335+ view.SetCursor(chatView.Cursor())
336+ return view
247337}
248338
249−func (p *chatPage) setCompactMode(compact bool) tea.Cmd {
250− p.compactMode = compact
251− var cmds []tea.Cmd
339+func (p *chatPage) setCompactMode(compact bool) {
340+ if p.compact == compact {
341+ return
342+ }
343+ p.compact = compact
252344 if compact {
253− // add offset for the header
254− p.layout.SetOffset(0, 1)
255− // make space for the header
256− cmds = append(cmds, p.layout.SetSize(p.wWidth, p.wHeight-1))
257− // remove the sidebar
258− cmds = append(cmds, p.layout.ClearRightPanel())
259− return tea.Batch(cmds...)
345+ p.compact = true
346+ p.sidebar.SetCompactMode(true)
260347 } else {
261− // remove the offset for the header
262− p.layout.SetOffset(0, 0)
263− // restore the original size
264− cmds = append(cmds, p.layout.SetSize(p.wWidth, p.wHeight))
265− // set the sidebar
266− cmds = append(cmds, p.setSidebar())
267− l, cmd := p.layout.Update(chat.SessionSelectedMsg(p.session))
268− p.layout = l.(layout.SplitPaneLayout)
269− cmds = append(cmds, cmd)
348+ p.compact = false
349+ p.showingDetails = false
350+ p.sidebar.SetCompactMode(false)
351+ }
352+}
270353
271− return tea.Batch(cmds...)
354+func (p *chatPage) handleCompactMode(newWidth int) {
355+ if p.forceCompact {
356+ return
357+ }
358+ if newWidth < CompactModeBreakpoint && !p.compact {
359+ p.setCompactMode(true)
360+ }
361+ if newWidth >= CompactModeBreakpoint && p.compact {
362+ p.setCompactMode(false)
272363 }
273364}
274365
275−func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
366+func (p *chatPage) SetSize(width, height int) tea.Cmd {
367+ p.handleCompactMode(width)
368+ p.width = width
369+ p.height = height
276370 var cmds []tea.Cmd
277− if p.session.ID == "" {
278− session, err := p.app.Sessions.Create(context.Background(), "New Session")
279− if err != nil {
280− return util.ReportError(err)
371+ switch p.state {
372+ case ChatStateOnboarding, ChatStateInitProject:
373+ // here we should just have the splash screen
374+ cmds = append(cmds, p.splash.SetSize(width, height))
375+ case ChatStateNewMessage:
376+ cmds = append(cmds, p.splash.SetSize(width, height-EditorHeight))
377+ cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
378+ cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
379+ case ChatStateInSession:
380+ if p.compact {
381+ cmds = append(cmds, p.chat.SetSize(width, height-EditorHeight-HeaderHeight))
382+ // In compact mode, the sidebar is shown in the details section, the width needs to be adjusted for the padding and border
383+ p.detailsWidth = width - 2 // because of position
384+ cmds = append(cmds, p.sidebar.SetSize(p.detailsWidth-2, p.detailsHeight-2)) // adjust for border
385+ cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
386+ cmds = append(cmds, p.header.SetWidth(width-1))
387+ } else {
388+ cmds = append(cmds, p.chat.SetSize(width-SideBarWidth, height-EditorHeight))
389+ cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
390+ cmds = append(cmds, p.sidebar.SetSize(SideBarWidth, height-EditorHeight))
281391 }
392+ cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
393+ }
394+ return tea.Batch(cmds...)
395+}
282396
283− p.session = session
284− cmd := p.setMessages()
285− if cmd != nil {
286− cmds = append(cmds, cmd)
287− }
288− cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
397+func (p *chatPage) newSession() tea.Cmd {
398+ if p.state != ChatStateInSession {
399+ // Cannot start a new session if we are not in the session state
400+ return nil
289401 }
402+ // blank session
403+ p.session = session.Session{}
404+ p.state = ChatStateNewMessage
405+ p.focusedPane = PanelTypeEditor
406+ p.canceling = false
407+ // Reset the chat and editor components
408+ return tea.Batch(
409+ util.CmdHandler(chat.SessionClearedMsg{}),
410+ p.SetSize(p.width, p.height),
411+ )
412+}
290413
291− _, err := p.app.CoderAgent.Run(context.Background(), p.session.ID, text, attachments...)
292− if err != nil {
293− return util.ReportError(err)
414+func (p *chatPage) setSession(session session.Session) tea.Cmd {
415+ if p.session.ID == session.ID {
416+ return nil
294417 }
295− return tea.Batch(cmds...)
418+
419+ var cmds []tea.Cmd
420+ p.session = session
421+ // We want to first resize the components
422+ if p.state != ChatStateInSession {
423+ p.state = ChatStateInSession
424+ cmds = append(cmds, p.SetSize(p.width, p.height))
425+ }
426+ cmds = append(cmds, p.chat.SetSession(session))
427+ cmds = append(cmds, p.sidebar.SetSession(session))
428+ cmds = append(cmds, p.header.SetSession(session))
429+ cmds = append(cmds, p.editor.SetSession(session))
430+
431+ return tea.Sequence(cmds...)
296432}
297433
298−func (p *chatPage) SetSize(width, height int) tea.Cmd {
299− return p.layout.SetSize(width, height)
434+func (p *chatPage) changeFocus() {
435+ if p.state != ChatStateInSession {
436+ // Cannot change focus if we are not in the session state
437+ return
438+ }
439+ switch p.focusedPane {
440+ case PanelTypeChat:
441+ p.focusedPane = PanelTypeEditor
442+ case PanelTypeEditor:
443+ p.focusedPane = PanelTypeChat
444+ }
300445}
301446
302−func (p *chatPage) GetSize() (int, int) {
303− return p.layout.GetSize()
447+func (p *chatPage) cancel() tea.Cmd {
448+ if p.state != ChatStateInSession || !p.app.CoderAgent.IsBusy() {
449+ // Cannot cancel if we are not in the session state
450+ return nil
451+ }
452+
453+ // second press of cancel key will actually cancel the session
454+ if p.canceling {
455+ p.canceling = false
456+ p.app.CoderAgent.Cancel(p.session.ID)
457+ return nil
458+ }
459+
460+ p.canceling = true
461+ return cancelTimerCmd()
304462}
305463
306−func (p *chatPage) View() tea.View {
307− if !p.compactMode || p.session.ID == "" {
308− // If not in compact mode or there is no session, we don't show the header
309− return p.layout.View()
464+func (p *chatPage) showDetails() {
465+ if p.state != ChatStateInSession || !p.compact {
466+ // Cannot show details if we are not in the session state or if we are not in compact mode
467+ return
310468 }
311− layoutView := p.layout.View()
312− chatView := strings.Join(
313− []string{
314− p.header.View().String(),
315− layoutView.String(),
316− }, "\n",
317− )
318− layers := []*lipgloss.Layer{
319− lipgloss.NewLayer(chatView).X(0).Y(0),
469+ p.showingDetails = !p.showingDetails
470+ p.header.SetDetailsOpen(p.showingDetails)
471+}
472+
473+func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
474+ session := p.session
475+ var cmds []tea.Cmd
476+ if p.state != ChatStateInSession {
477+ // branch new session
478+ newSession, err := p.app.Sessions.Create(context.Background(), "New Session")
479+ if err != nil {
480+ return util.ReportError(err)
481+ }
482+ session = newSession
483+ cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
320484 }
321− if p.showDetails {
322− t := styles.CurrentTheme()
323− style := t.S().Base.
324− Border(lipgloss.RoundedBorder()).
325− BorderForeground(t.BorderFocus)
326− version := t.S().Subtle.Padding(0, 1).AlignHorizontal(lipgloss.Right).Width(p.wWidth - 4).Render(version.Version)
327− details := style.Render(
328− lipgloss.JoinVertical(
329− lipgloss.Left,
330− p.compactSidebar.View().String(),
331− version,
332− ),
333− )
334− layers = append(layers, lipgloss.NewLayer(details).X(1).Y(1))
485+ _, err := p.app.CoderAgent.Run(context.Background(), session.ID, text, attachments...)
486+ if err != nil {
487+ return util.ReportError(err)
335488 }
336− canvas := lipgloss.NewCanvas(
337− layers...,
338− )
339− view := tea.NewView(canvas.Render())
340− view.SetCursor(layoutView.Cursor())
341− return view
489+ return tea.Batch(cmds...)
342490}
343491
344492func (p *chatPage) Bindings() []key.Binding {
@@ -348,7 +496,7 @@ func (p *chatPage) Bindings() []key.Binding {
348496 }
349497 if p.app.CoderAgent.IsBusy() {
350498 cancelBinding := p.keyMap.Cancel
351− if p.cancelPending {
499+ if p.canceling {
352500 cancelBinding = key.NewBinding(
353501 key.WithKeys("esc"),
354502 key.WithHelp("esc", "press again to cancel"),
@@ -357,56 +505,26 @@ func (p *chatPage) Bindings() []key.Binding {
357505 bindings = append([]key.Binding{cancelBinding}, bindings...)
358506 }
359507
360− if p.chatFocused {
508+ switch p.focusedPane {
509+ case PanelTypeChat:
361510 bindings = append([]key.Binding{
362511 key.NewBinding(
363512 key.WithKeys("tab"),
364513 key.WithHelp("tab", "focus editor"),
365514 ),
366515 }, bindings...)
367− } else {
516+ bindings = append(bindings, p.chat.Bindings()...)
517+ case PanelTypeEditor:
368518 bindings = append([]key.Binding{
369519 key.NewBinding(
370520 key.WithKeys("tab"),
371521 key.WithHelp("tab", "focus chat"),
372522 ),
373523 }, bindings...)
524+ bindings = append(bindings, p.editor.Bindings()...)
525+ case PanelTypeSplash:
526+ bindings = append(bindings, p.splash.Bindings()...)
374527 }
375528
376− bindings = append(bindings, p.layout.Bindings()...)
377529 return bindings
378530}
379−
380−func sidebarCmp(app *app.App, compact bool, session session.Session) layout.Container {
381− padding := layout.WithPadding(1, 1, 1, 1)
382− if compact {
383− padding = layout.WithPadding(0, 1, 1, 1)
384− }
385− sidebar := sidebar.NewSidebarCmp(app.History, app.LSPClients, compact)
386− if session.ID != "" {
387− sidebar.SetSession(session)
388− }
389−
390− return layout.NewContainer(
391− sidebar,
392− padding,
393− )
394−}
395−
396−func NewChatPage(app *app.App) ChatPage {
397− editorContainer := layout.NewContainer(
398− editor.NewEditorCmp(app),
399− )
400− return &chatPage{
401− app: app,
402− layout: layout.NewSplitPane(
403− layout.WithRightPanel(sidebarCmp(app, false, session.Session{})),
404− layout.WithBottomPanel(editorContainer),
405− layout.WithFixedBottomHeight(5),
406− layout.WithFixedRightWidth(31),
407− ),
408− compactSidebar: sidebarCmp(app, true, session.Session{}),
409− keyMap: DefaultKeyMap(),
410− header: header.New(app.LSPClients),
411− }
412−}