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