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.toggleDetails()
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.sidebar.SetCompactMode(true)
419 } else {
420 p.setShowDetails(false)
421 }
422}
423
424func (p *chatPage) handleCompactMode(newWidth int, newHeight int) {
425 if p.forceCompact {
426 return
427 }
428 if (newWidth < CompactModeWidthBreakpoint || newHeight < CompactModeHeightBreakpoint) && !p.compact {
429 p.setCompactMode(true)
430 }
431 if (newWidth >= CompactModeWidthBreakpoint && newHeight >= CompactModeHeightBreakpoint) && p.compact {
432 p.setCompactMode(false)
433 }
434}
435
436func (p *chatPage) SetSize(width, height int) tea.Cmd {
437 p.handleCompactMode(width, height)
438 p.width = width
439 p.height = height
440 var cmds []tea.Cmd
441
442 if p.session.ID == "" {
443 if p.splashFullScreen {
444 cmds = append(cmds, p.splash.SetSize(width, height))
445 } else {
446 cmds = append(cmds, p.splash.SetSize(width, height-EditorHeight))
447 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
448 cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
449 }
450 } else {
451 if p.compact {
452 cmds = append(cmds, p.chat.SetSize(width, height-EditorHeight-HeaderHeight))
453 p.detailsWidth = width - DetailsPositioning
454 cmds = append(cmds, p.sidebar.SetSize(p.detailsWidth-LeftRightBorders, p.detailsHeight-TopBottomBorders))
455 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
456 cmds = append(cmds, p.header.SetWidth(width-BorderWidth))
457 } else {
458 cmds = append(cmds, p.chat.SetSize(width-SideBarWidth, height-EditorHeight))
459 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
460 cmds = append(cmds, p.sidebar.SetSize(SideBarWidth, height-EditorHeight))
461 }
462 cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
463 }
464 return tea.Batch(cmds...)
465}
466
467func (p *chatPage) newSession() tea.Cmd {
468 if p.session.ID == "" {
469 return nil
470 }
471
472 p.session = session.Session{}
473 p.focusedPane = PanelTypeEditor
474 p.isCanceling = false
475 return tea.Batch(
476 util.CmdHandler(chat.SessionClearedMsg{}),
477 p.SetSize(p.width, p.height),
478 )
479}
480
481func (p *chatPage) setSession(session session.Session) tea.Cmd {
482 if p.session.ID == session.ID {
483 return nil
484 }
485
486 var cmds []tea.Cmd
487 p.session = session
488
489 cmds = append(cmds, p.SetSize(p.width, p.height))
490 cmds = append(cmds, p.chat.SetSession(session))
491 cmds = append(cmds, p.sidebar.SetSession(session))
492 cmds = append(cmds, p.header.SetSession(session))
493 cmds = append(cmds, p.editor.SetSession(session))
494
495 return tea.Sequence(cmds...)
496}
497
498func (p *chatPage) changeFocus() {
499 if p.session.ID == "" {
500 return
501 }
502 switch p.focusedPane {
503 case PanelTypeChat:
504 p.focusedPane = PanelTypeEditor
505 p.editor.Focus()
506 p.chat.Blur()
507 case PanelTypeEditor:
508 p.focusedPane = PanelTypeChat
509 p.chat.Focus()
510 p.editor.Blur()
511 }
512}
513
514func (p *chatPage) cancel() tea.Cmd {
515 if p.isCanceling {
516 p.isCanceling = false
517 p.app.CoderAgent.Cancel(p.session.ID)
518 return nil
519 }
520
521 p.isCanceling = true
522 return cancelTimerCmd()
523}
524
525func (p *chatPage) setShowDetails(show bool) {
526 p.showingDetails = show
527 p.header.SetDetailsOpen(p.showingDetails)
528 if !p.compact {
529 p.sidebar.SetCompactMode(false)
530 }
531}
532
533func (p *chatPage) toggleDetails() {
534 if p.session.ID == "" || !p.compact {
535 return
536 }
537 p.setShowDetails(!p.showingDetails)
538}
539
540func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
541 session := p.session
542 var cmds []tea.Cmd
543 if p.session.ID == "" {
544 newSession, err := p.app.Sessions.Create(context.Background(), "New Session")
545 if err != nil {
546 return util.ReportError(err)
547 }
548 session = newSession
549 cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
550 }
551 _, err := p.app.CoderAgent.Run(context.Background(), session.ID, text, attachments...)
552 if err != nil {
553 return util.ReportError(err)
554 }
555 return tea.Batch(cmds...)
556}
557
558func (p *chatPage) Bindings() []key.Binding {
559 bindings := []key.Binding{
560 p.keyMap.NewSession,
561 p.keyMap.AddAttachment,
562 }
563 if p.app.CoderAgent != nil && p.app.CoderAgent.IsBusy() {
564 cancelBinding := p.keyMap.Cancel
565 if p.isCanceling {
566 cancelBinding = key.NewBinding(
567 key.WithKeys("esc"),
568 key.WithHelp("esc", "press again to cancel"),
569 )
570 }
571 bindings = append([]key.Binding{cancelBinding}, bindings...)
572 }
573
574 switch p.focusedPane {
575 case PanelTypeChat:
576 bindings = append([]key.Binding{
577 key.NewBinding(
578 key.WithKeys("tab"),
579 key.WithHelp("tab", "focus editor"),
580 ),
581 }, bindings...)
582 bindings = append(bindings, p.chat.Bindings()...)
583 case PanelTypeEditor:
584 bindings = append([]key.Binding{
585 key.NewBinding(
586 key.WithKeys("tab"),
587 key.WithHelp("tab", "focus chat"),
588 ),
589 }, bindings...)
590 bindings = append(bindings, p.editor.Bindings()...)
591 case PanelTypeSplash:
592 bindings = append(bindings, p.splash.Bindings()...)
593 }
594
595 return bindings
596}
597
598func (p *chatPage) Help() help.KeyMap {
599 var shortList []key.Binding
600 var fullList [][]key.Binding
601 switch {
602 case p.isOnboarding && !p.splash.IsShowingAPIKey():
603 shortList = append(shortList,
604 // Choose model
605 key.NewBinding(
606 key.WithKeys("up", "down"),
607 key.WithHelp("↑/↓", "choose"),
608 ),
609 // Accept selection
610 key.NewBinding(
611 key.WithKeys("enter", "ctrl+y"),
612 key.WithHelp("enter", "accept"),
613 ),
614 // Quit
615 key.NewBinding(
616 key.WithKeys("ctrl+c"),
617 key.WithHelp("ctrl+c", "quit"),
618 ),
619 )
620 // keep them the same
621 for _, v := range shortList {
622 fullList = append(fullList, []key.Binding{v})
623 }
624 case p.isOnboarding && p.splash.IsShowingAPIKey():
625 shortList = append(shortList,
626 // Go back
627 key.NewBinding(
628 key.WithKeys("esc"),
629 key.WithHelp("esc", "back"),
630 ),
631 // Quit
632 key.NewBinding(
633 key.WithKeys("ctrl+c"),
634 key.WithHelp("ctrl+c", "quit"),
635 ),
636 )
637 // keep them the same
638 for _, v := range shortList {
639 fullList = append(fullList, []key.Binding{v})
640 }
641 case p.isProjectInit:
642 shortList = append(shortList,
643 key.NewBinding(
644 key.WithKeys("ctrl+c"),
645 key.WithHelp("ctrl+c", "quit"),
646 ),
647 )
648 // keep them the same
649 for _, v := range shortList {
650 fullList = append(fullList, []key.Binding{v})
651 }
652 default:
653 if p.editor.IsCompletionsOpen() {
654 shortList = append(shortList,
655 key.NewBinding(
656 key.WithKeys("tab", "enter"),
657 key.WithHelp("tab/enter", "complete"),
658 ),
659 key.NewBinding(
660 key.WithKeys("esc"),
661 key.WithHelp("esc", "cancel"),
662 ),
663 key.NewBinding(
664 key.WithKeys("up", "down"),
665 key.WithHelp("↑/↓", "choose"),
666 ),
667 )
668 for _, v := range shortList {
669 fullList = append(fullList, []key.Binding{v})
670 }
671 return core.NewSimpleHelp(shortList, fullList)
672 }
673 if p.app.CoderAgent != nil && p.app.CoderAgent.IsBusy() {
674 cancelBinding := key.NewBinding(
675 key.WithKeys("esc"),
676 key.WithHelp("esc", "cancel"),
677 )
678 if p.isCanceling {
679 cancelBinding = key.NewBinding(
680 key.WithKeys("esc"),
681 key.WithHelp("esc", "press again to cancel"),
682 )
683 }
684 shortList = append(shortList, cancelBinding)
685 fullList = append(fullList,
686 []key.Binding{
687 cancelBinding,
688 },
689 )
690 }
691 globalBindings := []key.Binding{}
692 // we are in a session
693 if p.session.ID != "" {
694 tabKey := key.NewBinding(
695 key.WithKeys("tab"),
696 key.WithHelp("tab", "focus chat"),
697 )
698 if p.focusedPane == PanelTypeChat {
699 tabKey = key.NewBinding(
700 key.WithKeys("tab"),
701 key.WithHelp("tab", "focus editor"),
702 )
703 }
704 shortList = append(shortList, tabKey)
705 globalBindings = append(globalBindings, tabKey)
706 }
707 commandsBinding := key.NewBinding(
708 key.WithKeys("ctrl+p"),
709 key.WithHelp("ctrl+p", "commands"),
710 )
711 helpBinding := key.NewBinding(
712 key.WithKeys("ctrl+g"),
713 key.WithHelp("ctrl+g", "more"),
714 )
715 globalBindings = append(globalBindings, commandsBinding)
716 globalBindings = append(globalBindings,
717 key.NewBinding(
718 key.WithKeys("ctrl+s"),
719 key.WithHelp("ctrl+s", "sessions"),
720 ),
721 )
722 if p.session.ID != "" {
723 globalBindings = append(globalBindings,
724 key.NewBinding(
725 key.WithKeys("ctrl+n"),
726 key.WithHelp("ctrl+n", "new sessions"),
727 ))
728 }
729 shortList = append(shortList,
730 // Commands
731 commandsBinding,
732 )
733 fullList = append(fullList, globalBindings)
734
735 switch p.focusedPane {
736 case PanelTypeChat:
737 shortList = append(shortList,
738 key.NewBinding(
739 key.WithKeys("up", "down"),
740 key.WithHelp("↑↓", "scroll"),
741 ),
742 )
743 fullList = append(fullList,
744 []key.Binding{
745 key.NewBinding(
746 key.WithKeys("up", "down"),
747 key.WithHelp("↑↓", "scroll"),
748 ),
749 key.NewBinding(
750 key.WithKeys("shift+up", "shift+down"),
751 key.WithHelp("shift+↑↓", "next/prev item"),
752 ),
753 key.NewBinding(
754 key.WithKeys("pgup", "b"),
755 key.WithHelp("b/pgup", "page up"),
756 ),
757 key.NewBinding(
758 key.WithKeys("pgdown", " ", "f"),
759 key.WithHelp("f/pgdn", "page down"),
760 ),
761 },
762 []key.Binding{
763 key.NewBinding(
764 key.WithKeys("u"),
765 key.WithHelp("u", "half page up"),
766 ),
767 key.NewBinding(
768 key.WithKeys("d"),
769 key.WithHelp("d", "half page down"),
770 ),
771 key.NewBinding(
772 key.WithKeys("g", "home"),
773 key.WithHelp("g", "hone"),
774 ),
775 key.NewBinding(
776 key.WithKeys("G", "end"),
777 key.WithHelp("G", "end"),
778 ),
779 },
780 )
781 case PanelTypeEditor:
782 newLineBinding := key.NewBinding(
783 key.WithKeys("shift+enter", "ctrl+j"),
784 // "ctrl+j" is a common keybinding for newline in many editors. If
785 // the terminal supports "shift+enter", we substitute the help text
786 // to reflect that.
787 key.WithHelp("ctrl+j", "newline"),
788 )
789 if p.keyboardEnhancements.SupportsKeyDisambiguation() {
790 newLineBinding.SetHelp("shift+enter", newLineBinding.Help().Desc)
791 }
792 shortList = append(shortList, newLineBinding)
793 fullList = append(fullList,
794 []key.Binding{
795 newLineBinding,
796 key.NewBinding(
797 key.WithKeys("ctrl+f"),
798 key.WithHelp("ctrl+f", "add image"),
799 ),
800 key.NewBinding(
801 key.WithKeys("/"),
802 key.WithHelp("/", "add file"),
803 ),
804 key.NewBinding(
805 key.WithKeys("ctrl+v"),
806 key.WithHelp("ctrl+v", "open editor"),
807 ),
808 })
809 }
810 shortList = append(shortList,
811 // Quit
812 key.NewBinding(
813 key.WithKeys("ctrl+c"),
814 key.WithHelp("ctrl+c", "quit"),
815 ),
816 // Help
817 helpBinding,
818 )
819 fullList = append(fullList, []key.Binding{
820 key.NewBinding(
821 key.WithKeys("ctrl+g"),
822 key.WithHelp("ctrl+g", "less"),
823 ),
824 })
825 }
826
827 return core.NewSimpleHelp(shortList, fullList)
828}
829
830func (p *chatPage) IsChatFocused() bool {
831 return p.focusedPane == PanelTypeChat
832}