1package chat
2
3import (
4 "context"
5 "runtime"
6 "time"
7
8 "github.com/charmbracelet/bubbles/v2/help"
9 "github.com/charmbracelet/bubbles/v2/key"
10 "github.com/charmbracelet/bubbles/v2/spinner"
11 tea "github.com/charmbracelet/bubbletea/v2"
12 "github.com/charmbracelet/crush/internal/app"
13 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/history"
15 "github.com/charmbracelet/crush/internal/message"
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/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 return p, p.SetSize(msg.Width, msg.Height)
169 case CancelTimerExpiredMsg:
170 p.isCanceling = false
171 return p, nil
172 case chat.SendMsg:
173 return p, p.sendMessage(msg.Text, msg.Attachments)
174 case chat.SessionSelectedMsg:
175 return p, p.setSession(msg)
176 case commands.ToggleCompactModeMsg:
177 p.forceCompact = !p.forceCompact
178 var cmd tea.Cmd
179 if p.forceCompact {
180 p.setCompactMode(true)
181 cmd = p.updateCompactConfig(true)
182 } else if p.width >= CompactModeWidthBreakpoint && p.height >= CompactModeHeightBreakpoint {
183 p.setCompactMode(false)
184 cmd = p.updateCompactConfig(false)
185 }
186 return p, tea.Batch(p.SetSize(p.width, p.height), cmd)
187 case pubsub.Event[session.Session]:
188 u, cmd := p.header.Update(msg)
189 p.header = u.(header.Header)
190 cmds = append(cmds, cmd)
191 u, cmd = p.sidebar.Update(msg)
192 p.sidebar = u.(sidebar.Sidebar)
193 cmds = append(cmds, cmd)
194 return p, tea.Batch(cmds...)
195 case chat.SessionClearedMsg:
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 u, cmd = p.chat.Update(msg)
203 p.chat = u.(chat.MessageListCmp)
204 cmds = append(cmds, cmd)
205 return p, tea.Batch(cmds...)
206 case filepicker.FilePickedMsg,
207 completions.CompletionsClosedMsg,
208 completions.SelectCompletionMsg:
209 u, cmd := p.editor.Update(msg)
210 p.editor = u.(editor.Editor)
211 cmds = append(cmds, cmd)
212 return p, tea.Batch(cmds...)
213
214 case pubsub.Event[message.Message],
215 anim.StepMsg,
216 spinner.TickMsg:
217 u, cmd := p.chat.Update(msg)
218 p.chat = u.(chat.MessageListCmp)
219 cmds = append(cmds, cmd)
220 return p, tea.Batch(cmds...)
221
222 case pubsub.Event[history.File], sidebar.SessionFilesMsg:
223 u, cmd := p.sidebar.Update(msg)
224 p.sidebar = u.(sidebar.Sidebar)
225 cmds = append(cmds, cmd)
226 return p, tea.Batch(cmds...)
227
228 case commands.CommandRunCustomMsg:
229 if p.app.CoderAgent.IsBusy() {
230 return p, util.ReportWarn("Agent is busy, please wait before executing a command...")
231 }
232
233 cmd := p.sendMessage(msg.Content, nil)
234 if cmd != nil {
235 return p, cmd
236 }
237 case splash.OnboardingCompleteMsg:
238 p.splashFullScreen = false
239 if b, _ := config.ProjectNeedsInitialization(); b {
240 p.splash.SetProjectInit(true)
241 p.splashFullScreen = true
242 return p, p.SetSize(p.width, p.height)
243 }
244 err := p.app.InitCoderAgent()
245 if err != nil {
246 return p, util.ReportError(err)
247 }
248 p.isOnboarding = false
249 p.isProjectInit = false
250 p.focusedPane = PanelTypeEditor
251 return p, p.SetSize(p.width, p.height)
252 case tea.KeyPressMsg:
253 switch {
254 case key.Matches(msg, p.keyMap.NewSession):
255 return p, p.newSession()
256 case key.Matches(msg, p.keyMap.AddAttachment):
257 agentCfg := config.Get().Agents["coder"]
258 model := config.Get().GetModelByType(agentCfg.Model)
259 if model.SupportsImages {
260 return p, util.CmdHandler(OpenFilePickerMsg{})
261 } else {
262 return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Model)
263 }
264 case key.Matches(msg, p.keyMap.Tab):
265 if p.session.ID == "" {
266 u, cmd := p.splash.Update(msg)
267 p.splash = u.(splash.Splash)
268 return p, cmd
269 }
270 p.changeFocus()
271 return p, nil
272 case key.Matches(msg, p.keyMap.Cancel):
273 if p.session.ID != "" && p.app.CoderAgent.IsBusy() {
274 return p, p.cancel()
275 }
276 case key.Matches(msg, p.keyMap.Details):
277 p.showDetails()
278 return p, nil
279 }
280
281 switch p.focusedPane {
282 case PanelTypeChat:
283 u, cmd := p.chat.Update(msg)
284 p.chat = u.(chat.MessageListCmp)
285 cmds = append(cmds, cmd)
286 case PanelTypeEditor:
287 u, cmd := p.editor.Update(msg)
288 p.editor = u.(editor.Editor)
289 cmds = append(cmds, cmd)
290 case PanelTypeSplash:
291 u, cmd := p.splash.Update(msg)
292 p.splash = u.(splash.Splash)
293 cmds = append(cmds, cmd)
294 }
295 case tea.PasteMsg:
296 switch p.focusedPane {
297 case PanelTypeEditor:
298 u, cmd := p.editor.Update(msg)
299 p.editor = u.(editor.Editor)
300 cmds = append(cmds, cmd)
301 return p, tea.Batch(cmds...)
302 case PanelTypeChat:
303 u, cmd := p.chat.Update(msg)
304 p.chat = u.(chat.MessageListCmp)
305 cmds = append(cmds, cmd)
306 return p, tea.Batch(cmds...)
307 case PanelTypeSplash:
308 u, cmd := p.splash.Update(msg)
309 p.splash = u.(splash.Splash)
310 cmds = append(cmds, cmd)
311 return p, tea.Batch(cmds...)
312 }
313 }
314 return p, tea.Batch(cmds...)
315}
316
317func (p *chatPage) Cursor() *tea.Cursor {
318 if p.header.ShowingDetails() {
319 return nil
320 }
321 switch p.focusedPane {
322 case PanelTypeEditor:
323 return p.editor.Cursor()
324 case PanelTypeSplash:
325 return p.splash.Cursor()
326 default:
327 return nil
328 }
329}
330
331func (p *chatPage) View() string {
332 var chatView string
333 t := styles.CurrentTheme()
334
335 if p.session.ID == "" {
336 splashView := p.splash.View()
337 // Full screen during onboarding or project initialization
338 if p.splashFullScreen {
339 chatView = splashView
340 } else {
341 // Show splash + editor for new message state
342 editorView := p.editor.View()
343 chatView = lipgloss.JoinVertical(
344 lipgloss.Left,
345 t.S().Base.Render(splashView),
346 editorView,
347 )
348 }
349 } else {
350 messagesView := p.chat.View()
351 editorView := p.editor.View()
352 if p.compact {
353 headerView := p.header.View()
354 chatView = lipgloss.JoinVertical(
355 lipgloss.Left,
356 headerView,
357 messagesView,
358 editorView,
359 )
360 } else {
361 sidebarView := p.sidebar.View()
362 messages := lipgloss.JoinHorizontal(
363 lipgloss.Left,
364 messagesView,
365 sidebarView,
366 )
367 chatView = lipgloss.JoinVertical(
368 lipgloss.Left,
369 messages,
370 p.editor.View(),
371 )
372 }
373 }
374
375 layers := []*lipgloss.Layer{
376 lipgloss.NewLayer(chatView).X(0).Y(0),
377 }
378
379 if p.showingDetails {
380 style := t.S().Base.
381 Width(p.detailsWidth).
382 Border(lipgloss.RoundedBorder()).
383 BorderForeground(t.BorderFocus)
384 version := t.S().Subtle.Width(p.detailsWidth - 2).AlignHorizontal(lipgloss.Right).Render(version.Version)
385 details := style.Render(
386 lipgloss.JoinVertical(
387 lipgloss.Left,
388 p.sidebar.View(),
389 version,
390 ),
391 )
392 layers = append(layers, lipgloss.NewLayer(details).X(1).Y(1))
393 }
394 canvas := lipgloss.NewCanvas(
395 layers...,
396 )
397 return canvas.Render()
398}
399
400func (p *chatPage) updateCompactConfig(compact bool) tea.Cmd {
401 return func() tea.Msg {
402 err := config.Get().SetCompactMode(compact)
403 if err != nil {
404 return util.InfoMsg{
405 Type: util.InfoTypeError,
406 Msg: "Failed to update compact mode configuration: " + err.Error(),
407 }
408 }
409 return nil
410 }
411}
412
413func (p *chatPage) setCompactMode(compact bool) {
414 if p.compact == compact {
415 return
416 }
417 p.compact = compact
418 if compact {
419 p.compact = true
420 p.sidebar.SetCompactMode(true)
421 } else {
422 p.compact = false
423 p.showingDetails = false
424 p.sidebar.SetCompactMode(false)
425 }
426}
427
428func (p *chatPage) handleCompactMode(newWidth int, newHeight int) {
429 if p.forceCompact {
430 return
431 }
432 if (newWidth < CompactModeWidthBreakpoint || newHeight < CompactModeHeightBreakpoint) && !p.compact {
433 p.setCompactMode(true)
434 }
435 if (newWidth >= CompactModeWidthBreakpoint && newHeight >= CompactModeHeightBreakpoint) && p.compact {
436 p.setCompactMode(false)
437 }
438}
439
440func (p *chatPage) SetSize(width, height int) tea.Cmd {
441 p.handleCompactMode(width, height)
442 p.width = width
443 p.height = height
444 var cmds []tea.Cmd
445
446 if p.session.ID == "" {
447 if p.splashFullScreen {
448 cmds = append(cmds, p.splash.SetSize(width, height))
449 } else {
450 cmds = append(cmds, p.splash.SetSize(width, height-EditorHeight))
451 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
452 cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
453 }
454 } else {
455 if p.compact {
456 cmds = append(cmds, p.chat.SetSize(width, height-EditorHeight-HeaderHeight))
457 p.detailsWidth = width - DetailsPositioning
458 cmds = append(cmds, p.sidebar.SetSize(p.detailsWidth-LeftRightBorders, p.detailsHeight-TopBottomBorders))
459 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
460 cmds = append(cmds, p.header.SetWidth(width-BorderWidth))
461 } else {
462 cmds = append(cmds, p.chat.SetSize(width-SideBarWidth, height-EditorHeight))
463 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
464 cmds = append(cmds, p.sidebar.SetSize(SideBarWidth, height-EditorHeight))
465 }
466 cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
467 }
468 return tea.Batch(cmds...)
469}
470
471func (p *chatPage) newSession() tea.Cmd {
472 if p.session.ID == "" {
473 return nil
474 }
475
476 p.session = session.Session{}
477 p.focusedPane = PanelTypeEditor
478 p.isCanceling = false
479 return tea.Batch(
480 util.CmdHandler(chat.SessionClearedMsg{}),
481 p.SetSize(p.width, p.height),
482 )
483}
484
485func (p *chatPage) setSession(session session.Session) tea.Cmd {
486 if p.session.ID == session.ID {
487 return nil
488 }
489
490 var cmds []tea.Cmd
491 p.session = session
492
493 cmds = append(cmds, p.SetSize(p.width, p.height))
494 cmds = append(cmds, p.chat.SetSession(session))
495 cmds = append(cmds, p.sidebar.SetSession(session))
496 cmds = append(cmds, p.header.SetSession(session))
497 cmds = append(cmds, p.editor.SetSession(session))
498
499 return tea.Sequence(cmds...)
500}
501
502func (p *chatPage) changeFocus() {
503 if p.session.ID == "" {
504 return
505 }
506 switch p.focusedPane {
507 case PanelTypeChat:
508 p.focusedPane = PanelTypeEditor
509 p.editor.Focus()
510 p.chat.Blur()
511 case PanelTypeEditor:
512 p.focusedPane = PanelTypeChat
513 p.chat.Focus()
514 p.editor.Blur()
515 }
516}
517
518func (p *chatPage) cancel() tea.Cmd {
519 if p.isCanceling {
520 p.isCanceling = false
521 p.app.CoderAgent.Cancel(p.session.ID)
522 return nil
523 }
524
525 p.isCanceling = true
526 return cancelTimerCmd()
527}
528
529func (p *chatPage) showDetails() {
530 if p.session.ID == "" || !p.compact {
531 return
532 }
533 p.showingDetails = !p.showingDetails
534 p.header.SetDetailsOpen(p.showingDetails)
535}
536
537func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
538 session := p.session
539 var cmds []tea.Cmd
540 if p.session.ID == "" {
541 newSession, err := p.app.Sessions.Create(context.Background(), "New Session")
542 if err != nil {
543 return util.ReportError(err)
544 }
545 session = newSession
546 cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
547 }
548 _, err := p.app.CoderAgent.Run(context.Background(), session.ID, text, attachments...)
549 if err != nil {
550 return util.ReportError(err)
551 }
552 return tea.Batch(cmds...)
553}
554
555func (p *chatPage) Bindings() []key.Binding {
556 bindings := []key.Binding{
557 p.keyMap.NewSession,
558 p.keyMap.AddAttachment,
559 }
560 if p.app.CoderAgent != nil && p.app.CoderAgent.IsBusy() {
561 cancelBinding := p.keyMap.Cancel
562 if p.isCanceling {
563 cancelBinding = key.NewBinding(
564 key.WithKeys("esc"),
565 key.WithHelp("esc", "press again to cancel"),
566 )
567 }
568 bindings = append([]key.Binding{cancelBinding}, bindings...)
569 }
570
571 switch p.focusedPane {
572 case PanelTypeChat:
573 bindings = append([]key.Binding{
574 key.NewBinding(
575 key.WithKeys("tab"),
576 key.WithHelp("tab", "focus editor"),
577 ),
578 }, bindings...)
579 bindings = append(bindings, p.chat.Bindings()...)
580 case PanelTypeEditor:
581 bindings = append([]key.Binding{
582 key.NewBinding(
583 key.WithKeys("tab"),
584 key.WithHelp("tab", "focus chat"),
585 ),
586 }, bindings...)
587 bindings = append(bindings, p.editor.Bindings()...)
588 case PanelTypeSplash:
589 bindings = append(bindings, p.splash.Bindings()...)
590 }
591
592 return bindings
593}
594
595func (a *chatPage) Help() help.KeyMap {
596 var shortList []key.Binding
597 var fullList [][]key.Binding
598 switch {
599 case a.isOnboarding && !a.splash.IsShowingAPIKey():
600 shortList = append(shortList,
601 // Choose model
602 key.NewBinding(
603 key.WithKeys("up", "down"),
604 key.WithHelp("↑/↓", "choose"),
605 ),
606 // Accept selection
607 key.NewBinding(
608 key.WithKeys("enter", "ctrl+y"),
609 key.WithHelp("enter", "accept"),
610 ),
611 // Quit
612 key.NewBinding(
613 key.WithKeys("ctrl+c"),
614 key.WithHelp("ctrl+c", "quit"),
615 ),
616 )
617 // keep them the same
618 for _, v := range shortList {
619 fullList = append(fullList, []key.Binding{v})
620 }
621 case a.isOnboarding && a.splash.IsShowingAPIKey():
622 var pasteKey key.Binding
623 if runtime.GOOS != "darwin" {
624 pasteKey = key.NewBinding(
625 key.WithKeys("ctrl+v"),
626 key.WithHelp("ctrl+v", "paste API key"),
627 )
628 } else {
629 pasteKey = key.NewBinding(
630 key.WithKeys("cmd+v"),
631 key.WithHelp("cmd+v", "paste API key"),
632 )
633 }
634 shortList = append(shortList,
635 // Go back
636 key.NewBinding(
637 key.WithKeys("esc"),
638 key.WithHelp("esc", "back"),
639 ),
640 // Paste
641 pasteKey,
642 // Quit
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 case a.isProjectInit:
653 shortList = append(shortList,
654 key.NewBinding(
655 key.WithKeys("ctrl+c"),
656 key.WithHelp("ctrl+c", "quit"),
657 ),
658 )
659 // keep them the same
660 for _, v := range shortList {
661 fullList = append(fullList, []key.Binding{v})
662 }
663 default:
664 if a.editor.IsCompletionsOpen() {
665 shortList = append(shortList,
666 key.NewBinding(
667 key.WithKeys("tab", "enter"),
668 key.WithHelp("tab/enter", "complete"),
669 ),
670 key.NewBinding(
671 key.WithKeys("esc"),
672 key.WithHelp("esc", "cancel"),
673 ),
674 key.NewBinding(
675 key.WithKeys("up", "down"),
676 key.WithHelp("↑/↓", "choose"),
677 ),
678 )
679 for _, v := range shortList {
680 fullList = append(fullList, []key.Binding{v})
681 }
682 return core.NewSimpleHelp(shortList, fullList)
683 }
684 if a.app.CoderAgent != nil && a.app.CoderAgent.IsBusy() {
685 cancelBinding := key.NewBinding(
686 key.WithKeys("esc"),
687 key.WithHelp("esc", "cancel"),
688 )
689 if a.isCanceling {
690 cancelBinding = key.NewBinding(
691 key.WithKeys("esc"),
692 key.WithHelp("esc", "press again to cancel"),
693 )
694 }
695 shortList = append(shortList, cancelBinding)
696 fullList = append(fullList,
697 []key.Binding{
698 cancelBinding,
699 },
700 )
701 }
702 globalBindings := []key.Binding{}
703 // we are in a session
704 if a.session.ID != "" {
705 tabKey := key.NewBinding(
706 key.WithKeys("tab"),
707 key.WithHelp("tab", "focus chat"),
708 )
709 if a.focusedPane == PanelTypeChat {
710 tabKey = key.NewBinding(
711 key.WithKeys("tab"),
712 key.WithHelp("tab", "focus editor"),
713 )
714 }
715 shortList = append(shortList, tabKey)
716 globalBindings = append(globalBindings, tabKey)
717 }
718 commandsBinding := key.NewBinding(
719 key.WithKeys("ctrl+p"),
720 key.WithHelp("ctrl+p", "commands"),
721 )
722 helpBinding := key.NewBinding(
723 key.WithKeys("ctrl+g"),
724 key.WithHelp("ctrl+g", "more"),
725 )
726 globalBindings = append(globalBindings, commandsBinding)
727 globalBindings = append(globalBindings,
728 key.NewBinding(
729 key.WithKeys("ctrl+s"),
730 key.WithHelp("ctrl+s", "sessions"),
731 ),
732 )
733 if a.session.ID != "" {
734 globalBindings = append(globalBindings,
735 key.NewBinding(
736 key.WithKeys("ctrl+n"),
737 key.WithHelp("ctrl+n", "new sessions"),
738 ))
739 }
740 shortList = append(shortList,
741 // Commands
742 commandsBinding,
743 )
744 fullList = append(fullList, globalBindings)
745
746 if a.focusedPane == PanelTypeChat {
747 shortList = append(shortList,
748 key.NewBinding(
749 key.WithKeys("up", "down"),
750 key.WithHelp("↑↓", "scroll"),
751 ),
752 )
753 fullList = append(fullList,
754 []key.Binding{
755 key.NewBinding(
756 key.WithKeys("up", "down"),
757 key.WithHelp("↑↓", "scroll"),
758 ),
759 key.NewBinding(
760 key.WithKeys("shift+up", "shift+down"),
761 key.WithHelp("shift+↑↓", "next/prev item"),
762 ),
763 key.NewBinding(
764 key.WithKeys("pgup", "b"),
765 key.WithHelp("b/pgup", "page up"),
766 ),
767 key.NewBinding(
768 key.WithKeys("pgdown", " ", "f"),
769 key.WithHelp("f/pgdn", "page down"),
770 ),
771 },
772 []key.Binding{
773 key.NewBinding(
774 key.WithKeys("u"),
775 key.WithHelp("u", "half page up"),
776 ),
777 key.NewBinding(
778 key.WithKeys("d"),
779 key.WithHelp("d", "half page down"),
780 ),
781 key.NewBinding(
782 key.WithKeys("g", "home"),
783 key.WithHelp("g", "hone"),
784 ),
785 key.NewBinding(
786 key.WithKeys("G", "end"),
787 key.WithHelp("G", "end"),
788 ),
789 },
790 )
791 } else if a.focusedPane == PanelTypeEditor {
792 newLineBinding := key.NewBinding(
793 key.WithKeys("shift+enter", "ctrl+j"),
794 // "ctrl+j" is a common keybinding for newline in many editors. If
795 // the terminal supports "shift+enter", we substitute the help text
796 // to reflect that.
797 key.WithHelp("ctrl+j", "newline"),
798 )
799 if a.keyboardEnhancements.SupportsKeyDisambiguation() {
800 newLineBinding.SetHelp("shift+enter", newLineBinding.Help().Desc)
801 }
802 shortList = append(shortList, newLineBinding)
803 fullList = append(fullList,
804 []key.Binding{
805 newLineBinding,
806 key.NewBinding(
807 key.WithKeys("ctrl+f"),
808 key.WithHelp("ctrl+f", "add image"),
809 ),
810 key.NewBinding(
811 key.WithKeys("/"),
812 key.WithHelp("/", "add file"),
813 ),
814 key.NewBinding(
815 key.WithKeys("ctrl+v"),
816 key.WithHelp("ctrl+v", "open editor"),
817 ),
818 })
819 }
820 shortList = append(shortList,
821 // Quit
822 key.NewBinding(
823 key.WithKeys("ctrl+c"),
824 key.WithHelp("ctrl+c", "quit"),
825 ),
826 // Help
827 helpBinding,
828 )
829 fullList = append(fullList, []key.Binding{
830 key.NewBinding(
831 key.WithKeys("ctrl+g"),
832 key.WithHelp("ctrl+g", "less"),
833 ),
834 })
835 }
836
837 return core.NewSimpleHelp(shortList, fullList)
838}
839
840func (p *chatPage) IsChatFocused() bool {
841 return p.focusedPane == PanelTypeChat
842}