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