1package chat
2
3import (
4 "context"
5 "time"
6
7 "github.com/charmbracelet/bubbles/v2/help"
8 "github.com/charmbracelet/bubbles/v2/key"
9 "github.com/charmbracelet/bubbles/v2/spinner"
10 tea "github.com/charmbracelet/bubbletea/v2"
11 "github.com/charmbracelet/crush/internal/app"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/history"
14 "github.com/charmbracelet/crush/internal/message"
15 "github.com/charmbracelet/crush/internal/permission"
16 "github.com/charmbracelet/crush/internal/pubsub"
17 "github.com/charmbracelet/crush/internal/session"
18 "github.com/charmbracelet/crush/internal/tui/components/anim"
19 "github.com/charmbracelet/crush/internal/tui/components/chat"
20 "github.com/charmbracelet/crush/internal/tui/components/chat/editor"
21 "github.com/charmbracelet/crush/internal/tui/components/chat/header"
22 "github.com/charmbracelet/crush/internal/tui/components/chat/sidebar"
23 "github.com/charmbracelet/crush/internal/tui/components/chat/splash"
24 "github.com/charmbracelet/crush/internal/tui/components/completions"
25 "github.com/charmbracelet/crush/internal/tui/components/core"
26 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
27 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
28 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
29 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
30 "github.com/charmbracelet/crush/internal/tui/page"
31 "github.com/charmbracelet/crush/internal/tui/styles"
32 "github.com/charmbracelet/crush/internal/tui/util"
33 "github.com/charmbracelet/crush/internal/version"
34 "github.com/charmbracelet/lipgloss/v2"
35)
36
37var ChatPageID page.PageID = "chat"
38
39type (
40 OpenFilePickerMsg struct{}
41 ChatFocusedMsg struct {
42 Focused bool
43 }
44 CancelTimerExpiredMsg struct{}
45)
46
47type PanelType string
48
49const (
50 PanelTypeChat PanelType = "chat"
51 PanelTypeEditor PanelType = "editor"
52 PanelTypeSplash PanelType = "splash"
53)
54
55const (
56 CompactModeWidthBreakpoint = 120 // Width at which the chat page switches to compact mode
57 CompactModeHeightBreakpoint = 30 // Height at which the chat page switches to compact mode
58 EditorHeight = 5 // Height of the editor input area including padding
59 SideBarWidth = 31 // Width of the sidebar
60 SideBarDetailsPadding = 1 // Padding for the sidebar details section
61 HeaderHeight = 1 // Height of the header
62
63 // Layout constants for borders and padding
64 BorderWidth = 1 // Width of component borders
65 LeftRightBorders = 2 // Left + right border width (1 + 1)
66 TopBottomBorders = 2 // Top + bottom border width (1 + 1)
67 DetailsPositioning = 2 // Positioning adjustment for details panel
68
69 // Timing constants
70 CancelTimerDuration = 2 * time.Second // Duration before cancel timer expires
71)
72
73type ChatPage interface {
74 util.Model
75 layout.Help
76 IsChatFocused() bool
77}
78
79// cancelTimerCmd creates a command that expires the cancel timer
80func cancelTimerCmd() tea.Cmd {
81 return tea.Tick(CancelTimerDuration, func(time.Time) tea.Msg {
82 return CancelTimerExpiredMsg{}
83 })
84}
85
86type chatPage struct {
87 width, height int
88 detailsWidth, detailsHeight int
89 app *app.App
90 keyboardEnhancements tea.KeyboardEnhancementsMsg
91
92 // Layout state
93 compact bool
94 forceCompact bool
95 focusedPane PanelType
96
97 // Session
98 session session.Session
99 keyMap KeyMap
100
101 // Components
102 header header.Header
103 sidebar sidebar.Sidebar
104 chat chat.MessageListCmp
105 editor editor.Editor
106 splash splash.Splash
107
108 // Simple state flags
109 showingDetails bool
110 isCanceling bool
111 splashFullScreen bool
112 isOnboarding bool
113 isProjectInit bool
114}
115
116func New(app *app.App) ChatPage {
117 return &chatPage{
118 app: app,
119 keyMap: DefaultKeyMap(),
120 header: header.New(app.LSPClients),
121 sidebar: sidebar.New(app.History, app.LSPClients, false),
122 chat: chat.New(app),
123 editor: editor.New(app),
124 splash: splash.New(),
125 focusedPane: PanelTypeSplash,
126 }
127}
128
129func (p *chatPage) Init() tea.Cmd {
130 cfg := config.Get()
131 compact := cfg.Options.TUI.CompactMode
132 p.compact = compact
133 p.forceCompact = compact
134 p.sidebar.SetCompactMode(p.compact)
135
136 // Set splash state based on config
137 if !config.HasInitialDataConfig() {
138 // First-time setup: show model selection
139 p.splash.SetOnboarding(true)
140 p.isOnboarding = true
141 p.splashFullScreen = true
142 } else if b, _ := config.ProjectNeedsInitialization(); b {
143 // Project needs CRUSH.md initialization
144 p.splash.SetProjectInit(true)
145 p.isProjectInit = true
146 p.splashFullScreen = true
147 } else {
148 // Ready to chat: focus editor, splash in background
149 p.focusedPane = PanelTypeEditor
150 p.splashFullScreen = false
151 }
152
153 return tea.Batch(
154 p.header.Init(),
155 p.sidebar.Init(),
156 p.chat.Init(),
157 p.editor.Init(),
158 p.splash.Init(),
159 )
160}
161
162func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
163 var cmds []tea.Cmd
164 switch msg := msg.(type) {
165 case tea.KeyboardEnhancementsMsg:
166 p.keyboardEnhancements = msg
167 return p, nil
168 case tea.WindowSizeMsg:
169 return p, p.SetSize(msg.Width, msg.Height)
170 case CancelTimerExpiredMsg:
171 p.isCanceling = false
172 return p, nil
173 case chat.SendMsg:
174 return p, p.sendMessage(msg.Text, msg.Attachments)
175 case chat.SessionSelectedMsg:
176 return p, p.setSession(msg)
177 case splash.SubmitAPIKeyMsg:
178 u, cmd := p.splash.Update(msg)
179 p.splash = u.(splash.Splash)
180 cmds = append(cmds, cmd)
181 return p, tea.Batch(cmds...)
182 case commands.ToggleCompactModeMsg:
183 p.forceCompact = !p.forceCompact
184 var cmd tea.Cmd
185 if p.forceCompact {
186 p.setCompactMode(true)
187 cmd = p.updateCompactConfig(true)
188 } else if p.width >= CompactModeWidthBreakpoint && p.height >= CompactModeHeightBreakpoint {
189 p.setCompactMode(false)
190 cmd = p.updateCompactConfig(false)
191 }
192 return p, tea.Batch(p.SetSize(p.width, p.height), cmd)
193 case commands.ToggleThinkingMsg:
194 return p, p.toggleThinking()
195 case pubsub.Event[session.Session]:
196 u, cmd := p.header.Update(msg)
197 p.header = u.(header.Header)
198 cmds = append(cmds, cmd)
199 u, cmd = p.sidebar.Update(msg)
200 p.sidebar = u.(sidebar.Sidebar)
201 cmds = append(cmds, cmd)
202 return p, tea.Batch(cmds...)
203 case chat.SessionClearedMsg:
204 u, cmd := p.header.Update(msg)
205 p.header = u.(header.Header)
206 cmds = append(cmds, cmd)
207 u, cmd = p.sidebar.Update(msg)
208 p.sidebar = u.(sidebar.Sidebar)
209 cmds = append(cmds, cmd)
210 u, cmd = p.chat.Update(msg)
211 p.chat = u.(chat.MessageListCmp)
212 cmds = append(cmds, cmd)
213 return p, tea.Batch(cmds...)
214 case filepicker.FilePickedMsg,
215 completions.CompletionsClosedMsg,
216 completions.SelectCompletionMsg:
217 u, cmd := p.editor.Update(msg)
218 p.editor = u.(editor.Editor)
219 cmds = append(cmds, cmd)
220 return p, tea.Batch(cmds...)
221
222 case models.APIKeyStateChangeMsg:
223 if p.focusedPane == PanelTypeSplash {
224 u, cmd := p.splash.Update(msg)
225 p.splash = u.(splash.Splash)
226 cmds = append(cmds, cmd)
227 }
228 return p, tea.Batch(cmds...)
229 case pubsub.Event[message.Message],
230 anim.StepMsg,
231 spinner.TickMsg:
232 if p.focusedPane == PanelTypeSplash {
233 u, cmd := p.splash.Update(msg)
234 p.splash = u.(splash.Splash)
235 cmds = append(cmds, cmd)
236 } else {
237 u, cmd := p.chat.Update(msg)
238 p.chat = u.(chat.MessageListCmp)
239 cmds = append(cmds, cmd)
240 }
241 return p, tea.Batch(cmds...)
242
243 case pubsub.Event[history.File], sidebar.SessionFilesMsg:
244 u, cmd := p.sidebar.Update(msg)
245 p.sidebar = u.(sidebar.Sidebar)
246 cmds = append(cmds, cmd)
247 return p, tea.Batch(cmds...)
248 case pubsub.Event[permission.PermissionNotification]:
249 u, cmd := p.chat.Update(msg)
250 p.chat = u.(chat.MessageListCmp)
251 cmds = append(cmds, cmd)
252 return p, tea.Batch(cmds...)
253
254 case commands.CommandRunCustomMsg:
255 if p.app.CoderAgent.IsBusy() {
256 return p, util.ReportWarn("Agent is busy, please wait before executing a command...")
257 }
258
259 cmd := p.sendMessage(msg.Content, nil)
260 if cmd != nil {
261 return p, cmd
262 }
263 case splash.OnboardingCompleteMsg:
264 p.splashFullScreen = false
265 if b, _ := config.ProjectNeedsInitialization(); b {
266 p.splash.SetProjectInit(true)
267 p.splashFullScreen = true
268 return p, p.SetSize(p.width, p.height)
269 }
270 err := p.app.InitCoderAgent()
271 if err != nil {
272 return p, util.ReportError(err)
273 }
274 p.isOnboarding = false
275 p.isProjectInit = false
276 p.focusedPane = PanelTypeEditor
277 return p, p.SetSize(p.width, p.height)
278 case tea.KeyPressMsg:
279 switch {
280 case key.Matches(msg, p.keyMap.NewSession):
281 return p, p.newSession()
282 case key.Matches(msg, p.keyMap.AddAttachment):
283 agentCfg := config.Get().Agents["coder"]
284 model := config.Get().GetModelByType(agentCfg.Model)
285 if model.SupportsImages {
286 return p, util.CmdHandler(OpenFilePickerMsg{})
287 } else {
288 return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Name)
289 }
290 case key.Matches(msg, p.keyMap.Tab):
291 if p.session.ID == "" {
292 u, cmd := p.splash.Update(msg)
293 p.splash = u.(splash.Splash)
294 return p, cmd
295 }
296 p.changeFocus()
297 return p, nil
298 case key.Matches(msg, p.keyMap.Cancel):
299 if p.session.ID != "" && p.app.CoderAgent.IsBusy() {
300 return p, p.cancel()
301 }
302 case key.Matches(msg, p.keyMap.Details):
303 p.toggleDetails()
304 return p, nil
305 }
306
307 switch p.focusedPane {
308 case PanelTypeChat:
309 u, cmd := p.chat.Update(msg)
310 p.chat = u.(chat.MessageListCmp)
311 cmds = append(cmds, cmd)
312 case PanelTypeEditor:
313 u, cmd := p.editor.Update(msg)
314 p.editor = u.(editor.Editor)
315 cmds = append(cmds, cmd)
316 case PanelTypeSplash:
317 u, cmd := p.splash.Update(msg)
318 p.splash = u.(splash.Splash)
319 cmds = append(cmds, cmd)
320 }
321 case tea.PasteMsg:
322 switch p.focusedPane {
323 case PanelTypeEditor:
324 u, cmd := p.editor.Update(msg)
325 p.editor = u.(editor.Editor)
326 cmds = append(cmds, cmd)
327 return p, tea.Batch(cmds...)
328 case PanelTypeChat:
329 u, cmd := p.chat.Update(msg)
330 p.chat = u.(chat.MessageListCmp)
331 cmds = append(cmds, cmd)
332 return p, tea.Batch(cmds...)
333 case PanelTypeSplash:
334 u, cmd := p.splash.Update(msg)
335 p.splash = u.(splash.Splash)
336 cmds = append(cmds, cmd)
337 return p, tea.Batch(cmds...)
338 }
339 }
340 return p, tea.Batch(cmds...)
341}
342
343func (p *chatPage) Cursor() *tea.Cursor {
344 if p.header.ShowingDetails() {
345 return nil
346 }
347 switch p.focusedPane {
348 case PanelTypeEditor:
349 return p.editor.Cursor()
350 case PanelTypeSplash:
351 return p.splash.Cursor()
352 default:
353 return nil
354 }
355}
356
357func (p *chatPage) View() string {
358 var chatView string
359 t := styles.CurrentTheme()
360
361 if p.session.ID == "" {
362 splashView := p.splash.View()
363 // Full screen during onboarding or project initialization
364 if p.splashFullScreen {
365 chatView = splashView
366 } else {
367 // Show splash + editor for new message state
368 editorView := p.editor.View()
369 chatView = lipgloss.JoinVertical(
370 lipgloss.Left,
371 t.S().Base.Render(splashView),
372 editorView,
373 )
374 }
375 } else {
376 messagesView := p.chat.View()
377 editorView := p.editor.View()
378 if p.compact {
379 headerView := p.header.View()
380 chatView = lipgloss.JoinVertical(
381 lipgloss.Left,
382 headerView,
383 messagesView,
384 editorView,
385 )
386 } else {
387 sidebarView := p.sidebar.View()
388 messages := lipgloss.JoinHorizontal(
389 lipgloss.Left,
390 messagesView,
391 sidebarView,
392 )
393 chatView = lipgloss.JoinVertical(
394 lipgloss.Left,
395 messages,
396 p.editor.View(),
397 )
398 }
399 }
400
401 layers := []*lipgloss.Layer{
402 lipgloss.NewLayer(chatView).X(0).Y(0),
403 }
404
405 if p.showingDetails {
406 style := t.S().Base.
407 Width(p.detailsWidth).
408 Border(lipgloss.RoundedBorder()).
409 BorderForeground(t.BorderFocus)
410 version := t.S().Base.Foreground(t.Border).Width(p.detailsWidth - 4).AlignHorizontal(lipgloss.Right).Render(version.Version)
411 details := style.Render(
412 lipgloss.JoinVertical(
413 lipgloss.Left,
414 p.sidebar.View(),
415 version,
416 ),
417 )
418 layers = append(layers, lipgloss.NewLayer(details).X(1).Y(1))
419 }
420 canvas := lipgloss.NewCanvas(
421 layers...,
422 )
423 return canvas.Render()
424}
425
426func (p *chatPage) updateCompactConfig(compact bool) tea.Cmd {
427 return func() tea.Msg {
428 err := config.Get().SetCompactMode(compact)
429 if err != nil {
430 return util.InfoMsg{
431 Type: util.InfoTypeError,
432 Msg: "Failed to update compact mode configuration: " + err.Error(),
433 }
434 }
435 return nil
436 }
437}
438
439func (p *chatPage) toggleThinking() tea.Cmd {
440 return func() tea.Msg {
441 cfg := config.Get()
442 agentCfg := cfg.Agents["coder"]
443 currentModel := cfg.Models[agentCfg.Model]
444
445 // Toggle the thinking mode
446 currentModel.Think = !currentModel.Think
447 cfg.Models[agentCfg.Model] = currentModel
448
449 // Update the agent with the new configuration
450 if err := p.app.UpdateAgentModel(); err != nil {
451 return util.InfoMsg{
452 Type: util.InfoTypeError,
453 Msg: "Failed to update thinking mode: " + err.Error(),
454 }
455 }
456
457 status := "disabled"
458 if currentModel.Think {
459 status = "enabled"
460 }
461 return util.InfoMsg{
462 Type: util.InfoTypeInfo,
463 Msg: "Thinking mode " + status,
464 }
465 }
466}
467
468func (p *chatPage) setCompactMode(compact bool) {
469 if p.compact == compact {
470 return
471 }
472 p.compact = compact
473 if compact {
474 p.sidebar.SetCompactMode(true)
475 } else {
476 p.setShowDetails(false)
477 }
478}
479
480func (p *chatPage) handleCompactMode(newWidth int, newHeight int) {
481 if p.forceCompact {
482 return
483 }
484 if (newWidth < CompactModeWidthBreakpoint || newHeight < CompactModeHeightBreakpoint) && !p.compact {
485 p.setCompactMode(true)
486 }
487 if (newWidth >= CompactModeWidthBreakpoint && newHeight >= CompactModeHeightBreakpoint) && p.compact {
488 p.setCompactMode(false)
489 }
490}
491
492func (p *chatPage) SetSize(width, height int) tea.Cmd {
493 p.handleCompactMode(width, height)
494 p.width = width
495 p.height = height
496 var cmds []tea.Cmd
497
498 if p.session.ID == "" {
499 if p.splashFullScreen {
500 cmds = append(cmds, p.splash.SetSize(width, height))
501 } else {
502 cmds = append(cmds, p.splash.SetSize(width, height-EditorHeight))
503 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
504 cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
505 }
506 } else {
507 if p.compact {
508 cmds = append(cmds, p.chat.SetSize(width, height-EditorHeight-HeaderHeight))
509 p.detailsWidth = width - DetailsPositioning
510 cmds = append(cmds, p.sidebar.SetSize(p.detailsWidth-LeftRightBorders, p.detailsHeight-TopBottomBorders))
511 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
512 cmds = append(cmds, p.header.SetWidth(width-BorderWidth))
513 } else {
514 cmds = append(cmds, p.chat.SetSize(width-SideBarWidth, height-EditorHeight))
515 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
516 cmds = append(cmds, p.sidebar.SetSize(SideBarWidth, height-EditorHeight))
517 }
518 cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
519 }
520 return tea.Batch(cmds...)
521}
522
523func (p *chatPage) newSession() tea.Cmd {
524 if p.session.ID == "" {
525 return nil
526 }
527
528 p.session = session.Session{}
529 p.focusedPane = PanelTypeEditor
530 p.editor.Focus()
531 p.chat.Blur()
532 p.isCanceling = false
533 return tea.Batch(
534 util.CmdHandler(chat.SessionClearedMsg{}),
535 p.SetSize(p.width, p.height),
536 )
537}
538
539func (p *chatPage) setSession(session session.Session) tea.Cmd {
540 if p.session.ID == session.ID {
541 return nil
542 }
543
544 var cmds []tea.Cmd
545 p.session = session
546
547 cmds = append(cmds, p.SetSize(p.width, p.height))
548 cmds = append(cmds, p.chat.SetSession(session))
549 cmds = append(cmds, p.sidebar.SetSession(session))
550 cmds = append(cmds, p.header.SetSession(session))
551 cmds = append(cmds, p.editor.SetSession(session))
552
553 return tea.Sequence(cmds...)
554}
555
556func (p *chatPage) changeFocus() {
557 if p.session.ID == "" {
558 return
559 }
560 switch p.focusedPane {
561 case PanelTypeChat:
562 p.focusedPane = PanelTypeEditor
563 p.editor.Focus()
564 p.chat.Blur()
565 case PanelTypeEditor:
566 p.focusedPane = PanelTypeChat
567 p.chat.Focus()
568 p.editor.Blur()
569 }
570}
571
572func (p *chatPage) cancel() tea.Cmd {
573 if p.isCanceling {
574 p.isCanceling = false
575 p.app.CoderAgent.Cancel(p.session.ID)
576 return nil
577 }
578
579 p.isCanceling = true
580 return cancelTimerCmd()
581}
582
583func (p *chatPage) setShowDetails(show bool) {
584 p.showingDetails = show
585 p.header.SetDetailsOpen(p.showingDetails)
586 if !p.compact {
587 p.sidebar.SetCompactMode(false)
588 }
589}
590
591func (p *chatPage) toggleDetails() {
592 if p.session.ID == "" || !p.compact {
593 return
594 }
595 p.setShowDetails(!p.showingDetails)
596}
597
598func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
599 session := p.session
600 var cmds []tea.Cmd
601 if p.session.ID == "" {
602 newSession, err := p.app.Sessions.Create(context.Background(), "New Session")
603 if err != nil {
604 return util.ReportError(err)
605 }
606 session = newSession
607 cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
608 }
609 _, err := p.app.CoderAgent.Run(context.Background(), session.ID, text, attachments...)
610 if err != nil {
611 return util.ReportError(err)
612 }
613 return tea.Batch(cmds...)
614}
615
616func (p *chatPage) Bindings() []key.Binding {
617 bindings := []key.Binding{
618 p.keyMap.NewSession,
619 p.keyMap.AddAttachment,
620 }
621 if p.app.CoderAgent != nil && p.app.CoderAgent.IsBusy() {
622 cancelBinding := p.keyMap.Cancel
623 if p.isCanceling {
624 cancelBinding = key.NewBinding(
625 key.WithKeys("esc"),
626 key.WithHelp("esc", "press again to cancel"),
627 )
628 }
629 bindings = append([]key.Binding{cancelBinding}, bindings...)
630 }
631
632 switch p.focusedPane {
633 case PanelTypeChat:
634 bindings = append([]key.Binding{
635 key.NewBinding(
636 key.WithKeys("tab"),
637 key.WithHelp("tab", "focus editor"),
638 ),
639 }, bindings...)
640 bindings = append(bindings, p.chat.Bindings()...)
641 case PanelTypeEditor:
642 bindings = append([]key.Binding{
643 key.NewBinding(
644 key.WithKeys("tab"),
645 key.WithHelp("tab", "focus chat"),
646 ),
647 }, bindings...)
648 bindings = append(bindings, p.editor.Bindings()...)
649 case PanelTypeSplash:
650 bindings = append(bindings, p.splash.Bindings()...)
651 }
652
653 return bindings
654}
655
656func (p *chatPage) Help() help.KeyMap {
657 var shortList []key.Binding
658 var fullList [][]key.Binding
659 switch {
660 case p.isOnboarding && !p.splash.IsShowingAPIKey():
661 shortList = append(shortList,
662 // Choose model
663 key.NewBinding(
664 key.WithKeys("up", "down"),
665 key.WithHelp("↑/↓", "choose"),
666 ),
667 // Accept selection
668 key.NewBinding(
669 key.WithKeys("enter", "ctrl+y"),
670 key.WithHelp("enter", "accept"),
671 ),
672 // Quit
673 key.NewBinding(
674 key.WithKeys("ctrl+c"),
675 key.WithHelp("ctrl+c", "quit"),
676 ),
677 )
678 // keep them the same
679 for _, v := range shortList {
680 fullList = append(fullList, []key.Binding{v})
681 }
682 case p.isOnboarding && p.splash.IsShowingAPIKey():
683 if p.splash.IsAPIKeyValid() {
684 shortList = append(shortList,
685 key.NewBinding(
686 key.WithKeys("enter"),
687 key.WithHelp("enter", "continue"),
688 ),
689 )
690 } else {
691 shortList = append(shortList,
692 // Go back
693 key.NewBinding(
694 key.WithKeys("esc"),
695 key.WithHelp("esc", "back"),
696 ),
697 )
698 }
699 shortList = append(shortList,
700 // Quit
701 key.NewBinding(
702 key.WithKeys("ctrl+c"),
703 key.WithHelp("ctrl+c", "quit"),
704 ),
705 )
706 // keep them the same
707 for _, v := range shortList {
708 fullList = append(fullList, []key.Binding{v})
709 }
710 case p.isProjectInit:
711 shortList = append(shortList,
712 key.NewBinding(
713 key.WithKeys("ctrl+c"),
714 key.WithHelp("ctrl+c", "quit"),
715 ),
716 )
717 // keep them the same
718 for _, v := range shortList {
719 fullList = append(fullList, []key.Binding{v})
720 }
721 default:
722 if p.editor.IsCompletionsOpen() {
723 shortList = append(shortList,
724 key.NewBinding(
725 key.WithKeys("tab", "enter"),
726 key.WithHelp("tab/enter", "complete"),
727 ),
728 key.NewBinding(
729 key.WithKeys("esc"),
730 key.WithHelp("esc", "cancel"),
731 ),
732 key.NewBinding(
733 key.WithKeys("up", "down"),
734 key.WithHelp("↑/↓", "choose"),
735 ),
736 )
737 for _, v := range shortList {
738 fullList = append(fullList, []key.Binding{v})
739 }
740 return core.NewSimpleHelp(shortList, fullList)
741 }
742 if p.app.CoderAgent != nil && p.app.CoderAgent.IsBusy() {
743 cancelBinding := key.NewBinding(
744 key.WithKeys("esc"),
745 key.WithHelp("esc", "cancel"),
746 )
747 if p.isCanceling {
748 cancelBinding = key.NewBinding(
749 key.WithKeys("esc"),
750 key.WithHelp("esc", "press again to cancel"),
751 )
752 }
753 shortList = append(shortList, cancelBinding)
754 fullList = append(fullList,
755 []key.Binding{
756 cancelBinding,
757 },
758 )
759 }
760 globalBindings := []key.Binding{}
761 // we are in a session
762 if p.session.ID != "" {
763 tabKey := key.NewBinding(
764 key.WithKeys("tab"),
765 key.WithHelp("tab", "focus chat"),
766 )
767 if p.focusedPane == PanelTypeChat {
768 tabKey = key.NewBinding(
769 key.WithKeys("tab"),
770 key.WithHelp("tab", "focus editor"),
771 )
772 }
773 shortList = append(shortList, tabKey)
774 globalBindings = append(globalBindings, tabKey)
775 }
776 commandsBinding := key.NewBinding(
777 key.WithKeys("ctrl+p"),
778 key.WithHelp("ctrl+p", "commands"),
779 )
780 helpBinding := key.NewBinding(
781 key.WithKeys("ctrl+g"),
782 key.WithHelp("ctrl+g", "more"),
783 )
784 globalBindings = append(globalBindings, commandsBinding)
785 globalBindings = append(globalBindings,
786 key.NewBinding(
787 key.WithKeys("ctrl+s"),
788 key.WithHelp("ctrl+s", "sessions"),
789 ),
790 )
791 if p.session.ID != "" {
792 globalBindings = append(globalBindings,
793 key.NewBinding(
794 key.WithKeys("ctrl+n"),
795 key.WithHelp("ctrl+n", "new sessions"),
796 ))
797 }
798 shortList = append(shortList,
799 // Commands
800 commandsBinding,
801 )
802 fullList = append(fullList, globalBindings)
803
804 switch p.focusedPane {
805 case PanelTypeChat:
806 shortList = append(shortList,
807 key.NewBinding(
808 key.WithKeys("up", "down"),
809 key.WithHelp("↑↓", "scroll"),
810 ),
811 )
812 fullList = append(fullList,
813 []key.Binding{
814 key.NewBinding(
815 key.WithKeys("up", "down"),
816 key.WithHelp("↑↓", "scroll"),
817 ),
818 key.NewBinding(
819 key.WithKeys("shift+up", "shift+down"),
820 key.WithHelp("shift+↑↓", "next/prev item"),
821 ),
822 key.NewBinding(
823 key.WithKeys("pgup", "b"),
824 key.WithHelp("b/pgup", "page up"),
825 ),
826 key.NewBinding(
827 key.WithKeys("pgdown", " ", "f"),
828 key.WithHelp("f/pgdn", "page down"),
829 ),
830 },
831 []key.Binding{
832 key.NewBinding(
833 key.WithKeys("u"),
834 key.WithHelp("u", "half page up"),
835 ),
836 key.NewBinding(
837 key.WithKeys("d"),
838 key.WithHelp("d", "half page down"),
839 ),
840 key.NewBinding(
841 key.WithKeys("g", "home"),
842 key.WithHelp("g", "hone"),
843 ),
844 key.NewBinding(
845 key.WithKeys("G", "end"),
846 key.WithHelp("G", "end"),
847 ),
848 },
849 )
850 case PanelTypeEditor:
851 newLineBinding := key.NewBinding(
852 key.WithKeys("shift+enter", "ctrl+j"),
853 // "ctrl+j" is a common keybinding for newline in many editors. If
854 // the terminal supports "shift+enter", we substitute the help text
855 // to reflect that.
856 key.WithHelp("ctrl+j", "newline"),
857 )
858 if p.keyboardEnhancements.SupportsKeyDisambiguation() {
859 newLineBinding.SetHelp("shift+enter", newLineBinding.Help().Desc)
860 }
861 shortList = append(shortList, newLineBinding)
862 fullList = append(fullList,
863 []key.Binding{
864 newLineBinding,
865 key.NewBinding(
866 key.WithKeys("ctrl+f"),
867 key.WithHelp("ctrl+f", "add image"),
868 ),
869 key.NewBinding(
870 key.WithKeys("/"),
871 key.WithHelp("/", "add file"),
872 ),
873 key.NewBinding(
874 key.WithKeys("ctrl+v"),
875 key.WithHelp("ctrl+v", "open editor"),
876 ),
877 key.NewBinding(
878 key.WithKeys("ctrl+r"),
879 key.WithHelp("ctrl+r+{i}", "delete attachment at index i"),
880 ),
881 key.NewBinding(
882 key.WithKeys("ctrl+r", "r"),
883 key.WithHelp("ctrl+r+r", "delete all attachments"),
884 ),
885 key.NewBinding(
886 key.WithKeys("esc"),
887 key.WithHelp("esc", "cancel delete mode"),
888 ),
889 })
890 }
891 shortList = append(shortList,
892 // Quit
893 key.NewBinding(
894 key.WithKeys("ctrl+c"),
895 key.WithHelp("ctrl+c", "quit"),
896 ),
897 // Help
898 helpBinding,
899 )
900 fullList = append(fullList, []key.Binding{
901 key.NewBinding(
902 key.WithKeys("ctrl+g"),
903 key.WithHelp("ctrl+g", "less"),
904 ),
905 })
906 }
907
908 return core.NewSimpleHelp(shortList, fullList)
909}
910
911func (p *chatPage) IsChatFocused() bool {
912 return p.focusedPane == PanelTypeChat
913}