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