@@ -2,21 +2,27 @@ package chat
8 "github.com/charmbracelet/bubbles/v2/key"
7 "github.com/charmbracelet/bubbles/v2/key"
8+ "github.com/charmbracelet/bubbles/v2/spinner"
9 tea "github.com/charmbracelet/bubbletea/v2"
9 tea "github.com/charmbracelet/bubbletea/v2"
10 "github.com/charmbracelet/crush/internal/app"
10 "github.com/charmbracelet/crush/internal/app"
11 "github.com/charmbracelet/crush/internal/config"
11 "github.com/charmbracelet/crush/internal/config"
12+ "github.com/charmbracelet/crush/internal/history"
12 "github.com/charmbracelet/crush/internal/message"
13 "github.com/charmbracelet/crush/internal/message"
14+ "github.com/charmbracelet/crush/internal/pubsub"
13 "github.com/charmbracelet/crush/internal/session"
15 "github.com/charmbracelet/crush/internal/session"
16+ "github.com/charmbracelet/crush/internal/tui/components/anim"
14 "github.com/charmbracelet/crush/internal/tui/components/chat"
17 "github.com/charmbracelet/crush/internal/tui/components/chat"
15 "github.com/charmbracelet/crush/internal/tui/components/chat/editor"
18 "github.com/charmbracelet/crush/internal/tui/components/chat/editor"
16 "github.com/charmbracelet/crush/internal/tui/components/chat/header"
19 "github.com/charmbracelet/crush/internal/tui/components/chat/header"
17 "github.com/charmbracelet/crush/internal/tui/components/chat/sidebar"
20 "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"
18 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
23 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
19 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
24 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
25+ "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
20 "github.com/charmbracelet/crush/internal/tui/page"
26 "github.com/charmbracelet/crush/internal/tui/page"
21 "github.com/charmbracelet/crush/internal/tui/styles"
27 "github.com/charmbracelet/crush/internal/tui/styles"
22 "github.com/charmbracelet/crush/internal/tui/util"
28 "github.com/charmbracelet/crush/internal/tui/util"
@@ -26,8 +32,6 @@ import (
27var ChatPageID page.PageID = "chat"
33var ChatPageID page.PageID = "chat"
29−const CompactModeBreakpoint = 120 // Width at which the chat page switches to compact mode
32 OpenFilePickerMsg struct{}
36 OpenFilePickerMsg struct{}
33 ChatFocusedMsg struct {
37 ChatFocusedMsg struct {
@@ -36,104 +40,171 @@ type (
36 CancelTimerExpiredMsg struct{}
40 CancelTimerExpiredMsg struct{}
46+ ChatStateOnboarding ChatState = "onboarding"
47+ ChatStateInitProject ChatState = "init_project"
48+ ChatStateNewMessage ChatState = "new_message"
49+ ChatStateInSession ChatState = "in_session"
55+ PanelTypeChat PanelType = "chat"
56+ PanelTypeEditor PanelType = "editor"
57+ PanelTypeSplash PanelType = "splash"
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
39type ChatPage interface {
68type ChatPage interface {
40 util.Model
69 util.Model
41 layout.Help
70 layout.Help
44−type chatPage struct {
73+// cancelTimerCmd creates a command that expires the cancel timer after 2 seconds
45− wWidth, wHeight int // Window dimensions
74+func cancelTimerCmd() tea.Cmd {
46− app *app.App
75+ return tea.Tick(2*time.Second, func(time.Time) tea.Msg {
47−
76+ return CancelTimerExpiredMsg{}
48− layout layout.SplitPaneLayout
77+ })
50− session session.Session
80+type chatPage struct {
82+ detailsWidth, detailsHeight int
85+ session session.Session
52− keyMap KeyMap
86+ keyMap KeyMap
87+ focusedPane PanelType
93+ sidebar sidebar.Sidebar
94+ chat chat.MessageListCmp
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
54− chatFocused bool
105+func New(app *app.App) ChatPage {
108+ state: ChatStateOnboarding,
56− compactMode bool
110+ keyMap: DefaultKeyMap(),
57− forceCompactMode bool // Force compact mode regardless of window size
58− showDetails bool // Show details in the header
60− compactSidebar layout.Container
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(),
65func (p *chatPage) Init() tea.Cmd {
120func (p *chatPage) Init() tea.Cmd {
123+ if b, _ := config.ProjectNeedsInitialization(); b {
124+ p.state = ChatStateInitProject
126+ p.state = ChatStateNewMessage
127+ p.focusedPane = PanelTypeEditor
131+ compact := cfg.Options.TUI.CompactMode
133+ p.forceCompact = compact
134+ p.sidebar.SetCompactMode(p.compact)
66 return tea.Batch(
135 return tea.Batch(
67− p.layout.Init(),
136+ p.header.Init(),
68− p.compactSidebar.Init(),
137+ p.sidebar.Init(),
69− p.layout.FocusPanel(layout.BottomPanel), // Focus on the bottom panel (editor),
138+ p.chat.Init(),
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{}
80func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
144func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
81 var cmds []tea.Cmd
145 var cmds []tea.Cmd
82 switch msg := msg.(type) {
146 switch msg := msg.(type) {
147+ case tea.WindowSizeMsg:
148+ return p, p.SetSize(msg.Width, msg.Height)
83 case CancelTimerExpiredMsg:
149 case CancelTimerExpiredMsg:
84− p.cancelPending = false
150+ p.canceling = false
85 return p, nil
151 return p, nil
86− case tea.WindowSizeMsg:
152+ case chat.SendMsg:
87− h, cmd := p.header.Update(msg)
153+ return p, p.sendMessage(msg.Text, msg.Attachments)
88− cmds = append(cmds, cmd)
154+ case chat.SessionSelectedMsg:
89− p.header = h.(header.Header)
155+ return p, p.setSession(msg)
90− cmds = append(cmds, p.compactSidebar.SetSize(msg.Width-4, 0))
156+ case commands.ToggleCompactModeMsg:
91− // the mode is only relevant when there is a session
157+ p.forceCompact = !p.forceCompact
92− if p.session.ID != "" {
158+ if p.forceCompact {
93− // Only auto-switch to compact mode if not forced
94− if !p.forceCompactMode {
95− if msg.Width <= CompactModeBreakpoint && p.wWidth > CompactModeBreakpoint {
97− p.wHeight = msg.Height
98− cmds = append(cmds, p.setCompactMode(true))
159+ p.setCompactMode(true)
99− return p, tea.Batch(cmds...)
100− } else if msg.Width > CompactModeBreakpoint && p.wWidth <= CompactModeBreakpoint {
160+ } else if p.width >= CompactModeBreakpoint {
101− p.wWidth = msg.Width
102− p.wHeight = msg.Height
103− return p, p.setCompactMode(false)
161+ p.setCompactMode(false)
107− p.wWidth = msg.Width
108− p.wHeight = msg.Height
109− layoutHeight := msg.Height
111− // make space for the header
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...)
192+ case pubsub.Event[message.Message],
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...)
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)
115 cmds = append(cmds, cmd)
205 cmds = append(cmds, cmd)
116 return p, tea.Batch(cmds...)
206 return p, tea.Batch(cmds...)
119− cmd := p.sendMessage(msg.Text, msg.Attachments)
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)
132− // Return to auto mode based on window size
133− shouldBeCompact := p.wWidth <= CompactModeBreakpoint
134− return p, p.setCompactMode(shouldBeCompact)
137 case commands.CommandRunCustomMsg:
208 case commands.CommandRunCustomMsg:
138 // Check if the agent is busy before executing custom commands
209 // Check if the agent is busy before executing custom commands
139 if p.app.CoderAgent.IsBusy() {
210 if p.app.CoderAgent.IsBusy() {
@@ -145,30 +216,10 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
145 if cmd != nil {
216 if cmd != nil {
146 return p, cmd
217 return p, cmd
148− case chat.SessionSelectedMsg:
149− if p.session.ID == "" {
150− cmd := p.setMessages()
152− cmds = append(cmds, cmd)
155− needsModeChange := p.session.ID == ""
157− p.header.SetSession(msg)
158− if needsModeChange && (p.wWidth <= CompactModeBreakpoint || p.forceCompactMode) {
159− cmds = append(cmds, p.setCompactMode(true))
161 case tea.KeyPressMsg:
219 case tea.KeyPressMsg:
163 case key.Matches(msg, p.keyMap.NewSession):
221 case key.Matches(msg, p.keyMap.NewSession):
164− p.session = session.Session{}
165− return p, tea.Batch(
222+ return p, p.newSession()
167− util.CmdHandler(chat.SessionClearedMsg{}),
168− p.setCompactMode(false),
169− p.layout.FocusPanel(layout.BottomPanel),
170− util.CmdHandler(ChatFocusedMsg{Focused: false}),
172 case key.Matches(msg, p.keyMap.AddAttachment):
223 case key.Matches(msg, p.keyMap.AddAttachment):
173 agentCfg := config.Get().Agents["coder"]
224 agentCfg := config.Get().Agents["coder"]
174 model := config.Get().GetModelByType(agentCfg.Model)
225 model := config.Get().GetModelByType(agentCfg.Model)
@@ -178,167 +229,264 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
178 return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Name)
229 return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Name)
180 case key.Matches(msg, p.keyMap.Tab):
231 case key.Matches(msg, p.keyMap.Tab):
181− if p.session.ID == "" {
232+ p.changeFocus()
182− return p, nil
233+ return p, nil
184− p.chatFocused = !p.chatFocused
186− cmds = append(cmds, p.layout.FocusPanel(layout.LeftPanel))
187− cmds = append(cmds, util.CmdHandler(ChatFocusedMsg{Focused: true}))
189− cmds = append(cmds, p.layout.FocusPanel(layout.BottomPanel))
190− cmds = append(cmds, util.CmdHandler(ChatFocusedMsg{Focused: false}))
192− return p, tea.Batch(cmds...)
193 case key.Matches(msg, p.keyMap.Cancel):
234 case key.Matches(msg, p.keyMap.Cancel):
194− if p.session.ID != "" {
235+ return p, p.cancel()
195− if p.cancelPending {
196− // Second ESC press - actually cancel the session
197− p.cancelPending = false
198− p.app.CoderAgent.Cancel(p.session.ID)
201− // First ESC press - start the timer
202− p.cancelPending = true
203− return p, p.cancelTimerCmd()
206 case key.Matches(msg, p.keyMap.Details):
236 case key.Matches(msg, p.keyMap.Details):
207− if p.session.ID == "" || !p.compactMode {
208− return p, nil // No session to show details for
210− p.showDetails = !p.showDetails
237+ p.showDetails()
211− p.header.SetDetailsOpen(p.showDetails)
213− return p, tea.Batch()
216 return p, nil
238 return p, nil
241+ // Send the key press to the focused pane
242+ switch p.focusedPane {
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)
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)
228 return p, tea.Batch(cmds...)
253 return p, tea.Batch(cmds...)
231−func (p *chatPage) setMessages() tea.Cmd {
256+func (p *chatPage) View() tea.View {
232− messagesContainer := layout.NewContainer(
257+ var chatView tea.View
233− chat.NewMessagesListCmp(p.app),
258+ t := styles.CurrentTheme()
234− layout.WithPadding(1, 1, 0, 1),
259+ switch p.state {
235− )
260+ case ChatStateOnboarding, ChatStateInitProject:
236− return tea.Batch(p.layout.SetLeftPanel(messagesContainer), messagesContainer.Init())
261+ chatView = tea.NewView(
263+ p.splash.View().String(),
266+ case ChatStateNewMessage:
267+ editorView := p.editor.View()
268+ chatView = tea.NewView(
269+ lipgloss.JoinVertical(
272+ p.splash.View().String(),
274+ editorView.String(),
277+ chatView.SetCursor(editorView.Cursor())
278+ case ChatStateInSession:
279+ messagesView := p.chat.View()
280+ editorView := p.editor.View()
282+ headerView := p.header.View()
283+ chatView = tea.NewView(
284+ lipgloss.JoinVertical(
286+ headerView.String(),
287+ messagesView.String(),
288+ editorView.String(),
291+ chatView.SetCursor(editorView.Cursor())
293+ sidebarView := p.sidebar.View()
294+ messages := lipgloss.JoinHorizontal(
296+ messagesView.String(),
297+ sidebarView.String(),
299+ chatView = tea.NewView(
300+ lipgloss.JoinVertical(
303+ p.editor.View().String(),
306+ chatView.SetCursor(editorView.Cursor())
309+ chatView = tea.NewView("Unknown chat state")
239−func (p *chatPage) setSidebar() tea.Cmd {
312+ layers := []*lipgloss.Layer{
240− sidebarContainer := sidebarCmp(p.app, false, p.session)
313+ lipgloss.NewLayer(chatView.String()).X(0).Y(0),
241− sidebarContainer.Init()
242− return p.layout.SetRightPanel(sidebarContainer)
245−func (p *chatPage) clearMessages() tea.Cmd {
316+ if p.showingDetails {
246− return p.layout.ClearLeftPanel()
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(
325+ p.sidebar.View().String(),
329+ layers = append(layers, lipgloss.NewLayer(details).X(1).Y(1))
331+ canvas := lipgloss.NewCanvas(
334+ view := tea.NewView(canvas.Render())
335+ view.SetCursor(chatView.Cursor())
249−func (p *chatPage) setCompactMode(compact bool) tea.Cmd {
339+func (p *chatPage) setCompactMode(compact bool) {
340+ if p.compact == compact {
250− p.compactMode = compact
343+ p.compact = compact
252 if compact {
344 if compact {
253− // add offset for the header
345+ p.compact = true
254− p.layout.SetOffset(0, 1)
346+ p.sidebar.SetCompactMode(true)
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...)
261− // remove the offset for the header
348+ p.compact = false
262− p.layout.SetOffset(0, 0)
349+ p.showingDetails = false
263− // restore the original size
350+ p.sidebar.SetCompactMode(false)
264− cmds = append(cmds, p.layout.SetSize(p.wWidth, p.wHeight))
351+ }
265− // set the sidebar
352+}
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)
354+func (p *chatPage) handleCompactMode(newWidth int) {
271− return tea.Batch(cmds...)
356+ return
358+ if newWidth < CompactModeBreakpoint && !p.compact {
359+ p.setCompactMode(true)
361+ if newWidth >= CompactModeBreakpoint && p.compact {
362+ p.setCompactMode(false)
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)
276 var cmds []tea.Cmd
370 var cmds []tea.Cmd
277− if p.session.ID == "" {
371+ switch p.state {
278− session, err := p.app.Sessions.Create(context.Background(), "New Session")
372+ case ChatStateOnboarding, ChatStateInitProject:
279− if err != nil {
373+ // here we should just have the splash screen
280− return util.ReportError(err)
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:
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))
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))
392+ cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
394+ return tea.Batch(cmds...)
283− p.session = session
397+func (p *chatPage) newSession() tea.Cmd {
284− cmd := p.setMessages()
398+ if p.state != ChatStateInSession {
285− if cmd != nil {
399+ // Cannot start a new session if we are not in the session state
286− cmds = append(cmds, cmd)
400+ return nil
288− cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
403+ p.session = session.Session{}
404+ p.state = ChatStateNewMessage
405+ p.focusedPane = PanelTypeEditor
407+ // Reset the chat and editor components
409+ util.CmdHandler(chat.SessionClearedMsg{}),
410+ p.SetSize(p.width, p.height),
291− _, err := p.app.CoderAgent.Run(context.Background(), p.session.ID, text, attachments...)
414+func (p *chatPage) setSession(session session.Session) tea.Cmd {
292− if err != nil {
415+ if p.session.ID == session.ID {
293− return util.ReportError(err)
416+ return nil
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))
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))
295− return tea.Batch(cmds...)
431+ return tea.Sequence(cmds...)
298−func (p *chatPage) SetSize(width, height int) tea.Cmd {
434+func (p *chatPage) changeFocus() {
299− return p.layout.SetSize(width, height)
435+ if p.state != ChatStateInSession {
436+ // Cannot change focus if we are not in the session state
439+ switch p.focusedPane {
441+ p.focusedPane = PanelTypeEditor
442+ case PanelTypeEditor:
443+ p.focusedPane = PanelTypeChat
302−func (p *chatPage) GetSize() (int, int) {
447+func (p *chatPage) cancel() tea.Cmd {
303− return p.layout.GetSize()
448+ if p.state != ChatStateInSession || !p.app.CoderAgent.IsBusy() {
449+ // Cannot cancel if we are not in the session state
453+ // second press of cancel key will actually cancel the session
456+ p.app.CoderAgent.Cancel(p.session.ID)
461+ return cancelTimerCmd()
306−func (p *chatPage) View() tea.View {
464+func (p *chatPage) showDetails() {
307− if !p.compactMode || p.session.ID == "" {
465+ if p.state != ChatStateInSession || !p.compact {
308− // If not in compact mode or there is no session, we don't show the header
466+ // Cannot show details if we are not in the session state or if we are not in compact mode
309− return p.layout.View()
467+ return
311− layoutView := p.layout.View()
469+ p.showingDetails = !p.showingDetails
312− chatView := strings.Join(
470+ p.header.SetDetailsOpen(p.showingDetails)
314− p.header.View().String(),
472+
315− layoutView.String(),
473+func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
316− }, "\n",
474+ session := p.session
317− )
475+ var cmds []tea.Cmd
318− layers := []*lipgloss.Layer{
476+ if p.state != ChatStateInSession {
319− lipgloss.NewLayer(chatView).X(0).Y(0),
477+ // branch new session
478+ newSession, err := p.app.Sessions.Create(context.Background(), "New Session")
480+ return util.ReportError(err)
482+ session = newSession
483+ cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
321− if p.showDetails {
485+ _, err := p.app.CoderAgent.Run(context.Background(), session.ID, text, attachments...)
322− t := styles.CurrentTheme()
486+ if err != nil {
323− style := t.S().Base.
487+ return util.ReportError(err)
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(
330− p.compactSidebar.View().String(),
334− layers = append(layers, lipgloss.NewLayer(details).X(1).Y(1))
336− canvas := lipgloss.NewCanvas(
489+ return tea.Batch(cmds...)
339− view := tea.NewView(canvas.Render())
340− view.SetCursor(layoutView.Cursor())
344func (p *chatPage) Bindings() []key.Binding {
492func (p *chatPage) Bindings() []key.Binding {
@@ -348,7 +496,7 @@ func (p *chatPage) Bindings() []key.Binding {
349 if p.app.CoderAgent.IsBusy() {
497 if p.app.CoderAgent.IsBusy() {
350 cancelBinding := p.keyMap.Cancel
498 cancelBinding := p.keyMap.Cancel
351− if p.cancelPending {
499+ if p.canceling {
352 cancelBinding = key.NewBinding(
500 cancelBinding = key.NewBinding(
353 key.WithKeys("esc"),
501 key.WithKeys("esc"),
354 key.WithHelp("esc", "press again to cancel"),
502 key.WithHelp("esc", "press again to cancel"),
@@ -357,56 +505,26 @@ func (p *chatPage) Bindings() []key.Binding {
357 bindings = append([]key.Binding{cancelBinding}, bindings...)
505 bindings = append([]key.Binding{cancelBinding}, bindings...)
360− if p.chatFocused {
508+ switch p.focusedPane {
361 bindings = append([]key.Binding{
510 bindings = append([]key.Binding{
362 key.NewBinding(
511 key.NewBinding(
363 key.WithKeys("tab"),
512 key.WithKeys("tab"),
364 key.WithHelp("tab", "focus editor"),
513 key.WithHelp("tab", "focus editor"),
366 }, bindings...)
515 }, bindings...)
367− } else {
516+ bindings = append(bindings, p.chat.Bindings()...)
517+ case PanelTypeEditor:
368 bindings = append([]key.Binding{
518 bindings = append([]key.Binding{
369 key.NewBinding(
519 key.NewBinding(
370 key.WithKeys("tab"),
520 key.WithKeys("tab"),
371 key.WithHelp("tab", "focus chat"),
521 key.WithHelp("tab", "focus chat"),
373 }, bindings...)
523 }, bindings...)
524+ bindings = append(bindings, p.editor.Bindings()...)
525+ case PanelTypeSplash:
526+ bindings = append(bindings, p.splash.Bindings()...)
376− bindings = append(bindings, p.layout.Bindings()...)
377 return bindings
529 return bindings
380−func sidebarCmp(app *app.App, compact bool, session session.Session) layout.Container {
381− padding := layout.WithPadding(1, 1, 1, 1)
383− padding = layout.WithPadding(0, 1, 1, 1)
385− sidebar := sidebar.NewSidebarCmp(app.History, app.LSPClients, compact)
386− if session.ID != "" {
387− sidebar.SetSession(session)
390− return layout.NewContainer(
396−func NewChatPage(app *app.App) ChatPage {
397− editorContainer := layout.NewContainer(
398− editor.NewEditorCmp(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),
408− compactSidebar: sidebarCmp(app, true, session.Session{}),
409− keyMap: DefaultKeyMap(),
410− header: header.New(app.LSPClients),