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