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 switch p.focusedPane {
319 case PanelTypeEditor:
320 return p.editor.Cursor()
321 case PanelTypeSplash:
322 return p.splash.Cursor()
323 default:
324 return nil
325 }
326}
327
328func (p *chatPage) View() string {
329 var chatView string
330 t := styles.CurrentTheme()
331
332 if p.session.ID == "" {
333 splashView := p.splash.View()
334 // Full screen during onboarding or project initialization
335 if p.splashFullScreen {
336 chatView = splashView
337 } else {
338 // Show splash + editor for new message state
339 editorView := p.editor.View()
340 chatView = lipgloss.JoinVertical(
341 lipgloss.Left,
342 t.S().Base.Render(splashView),
343 editorView,
344 )
345 }
346 } else {
347 messagesView := p.chat.View()
348 editorView := p.editor.View()
349 if p.compact {
350 headerView := p.header.View()
351 chatView = lipgloss.JoinVertical(
352 lipgloss.Left,
353 headerView,
354 messagesView,
355 editorView,
356 )
357 } else {
358 sidebarView := p.sidebar.View()
359 messages := lipgloss.JoinHorizontal(
360 lipgloss.Left,
361 messagesView,
362 sidebarView,
363 )
364 chatView = lipgloss.JoinVertical(
365 lipgloss.Left,
366 messages,
367 p.editor.View(),
368 )
369 }
370 }
371
372 layers := []*lipgloss.Layer{
373 lipgloss.NewLayer(chatView).X(0).Y(0),
374 }
375
376 if p.showingDetails {
377 style := t.S().Base.
378 Width(p.detailsWidth).
379 Border(lipgloss.RoundedBorder()).
380 BorderForeground(t.BorderFocus)
381 version := t.S().Subtle.Width(p.detailsWidth - 2).AlignHorizontal(lipgloss.Right).Render(version.Version)
382 details := style.Render(
383 lipgloss.JoinVertical(
384 lipgloss.Left,
385 p.sidebar.View(),
386 version,
387 ),
388 )
389 layers = append(layers, lipgloss.NewLayer(details).X(1).Y(1))
390 }
391 canvas := lipgloss.NewCanvas(
392 layers...,
393 )
394 return canvas.Render()
395}
396
397func (p *chatPage) updateCompactConfig(compact bool) tea.Cmd {
398 return func() tea.Msg {
399 err := config.Get().SetCompactMode(compact)
400 if err != nil {
401 return util.InfoMsg{
402 Type: util.InfoTypeError,
403 Msg: "Failed to update compact mode configuration: " + err.Error(),
404 }
405 }
406 return nil
407 }
408}
409
410func (p *chatPage) setCompactMode(compact bool) {
411 if p.compact == compact {
412 return
413 }
414 p.compact = compact
415 if compact {
416 p.compact = true
417 p.sidebar.SetCompactMode(true)
418 } else {
419 p.compact = false
420 p.showingDetails = false
421 p.sidebar.SetCompactMode(false)
422 }
423}
424
425func (p *chatPage) handleCompactMode(newWidth int, newHeight int) {
426 if p.forceCompact {
427 return
428 }
429 if (newWidth < CompactModeWidthBreakpoint || newHeight < CompactModeHeightBreakpoint) && !p.compact {
430 p.setCompactMode(true)
431 }
432 if (newWidth >= CompactModeWidthBreakpoint && newHeight >= CompactModeHeightBreakpoint) && p.compact {
433 p.setCompactMode(false)
434 }
435}
436
437func (p *chatPage) SetSize(width, height int) tea.Cmd {
438 p.handleCompactMode(width, height)
439 p.width = width
440 p.height = height
441 var cmds []tea.Cmd
442
443 if p.session.ID == "" {
444 if p.splashFullScreen {
445 cmds = append(cmds, p.splash.SetSize(width, height))
446 } else {
447 cmds = append(cmds, p.splash.SetSize(width, height-EditorHeight))
448 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
449 cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
450 }
451 } else {
452 if p.compact {
453 cmds = append(cmds, p.chat.SetSize(width, height-EditorHeight-HeaderHeight))
454 p.detailsWidth = width - DetailsPositioning
455 cmds = append(cmds, p.sidebar.SetSize(p.detailsWidth-LeftRightBorders, p.detailsHeight-TopBottomBorders))
456 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
457 cmds = append(cmds, p.header.SetWidth(width-BorderWidth))
458 } else {
459 cmds = append(cmds, p.chat.SetSize(width-SideBarWidth, height-EditorHeight))
460 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
461 cmds = append(cmds, p.sidebar.SetSize(SideBarWidth, height-EditorHeight))
462 }
463 cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
464 }
465 return tea.Batch(cmds...)
466}
467
468func (p *chatPage) newSession() tea.Cmd {
469 if p.session.ID == "" {
470 return nil
471 }
472
473 p.session = session.Session{}
474 p.focusedPane = PanelTypeEditor
475 p.isCanceling = false
476 return tea.Batch(
477 util.CmdHandler(chat.SessionClearedMsg{}),
478 p.SetSize(p.width, p.height),
479 )
480}
481
482func (p *chatPage) setSession(session session.Session) tea.Cmd {
483 if p.session.ID == session.ID {
484 return nil
485 }
486
487 var cmds []tea.Cmd
488 p.session = session
489
490 cmds = append(cmds, p.SetSize(p.width, p.height))
491 cmds = append(cmds, p.chat.SetSession(session))
492 cmds = append(cmds, p.sidebar.SetSession(session))
493 cmds = append(cmds, p.header.SetSession(session))
494 cmds = append(cmds, p.editor.SetSession(session))
495
496 return tea.Sequence(cmds...)
497}
498
499func (p *chatPage) changeFocus() {
500 if p.session.ID == "" {
501 return
502 }
503 switch p.focusedPane {
504 case PanelTypeChat:
505 p.focusedPane = PanelTypeEditor
506 p.editor.Focus()
507 p.chat.Blur()
508 case PanelTypeEditor:
509 p.focusedPane = PanelTypeChat
510 p.chat.Focus()
511 p.editor.Blur()
512 }
513}
514
515func (p *chatPage) cancel() tea.Cmd {
516 if p.isCanceling {
517 p.isCanceling = false
518 p.app.CoderAgent.Cancel(p.session.ID)
519 return nil
520 }
521
522 p.isCanceling = true
523 return cancelTimerCmd()
524}
525
526func (p *chatPage) showDetails() {
527 if p.session.ID == "" || !p.compact {
528 return
529 }
530 p.showingDetails = !p.showingDetails
531 p.header.SetDetailsOpen(p.showingDetails)
532}
533
534func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
535 session := p.session
536 var cmds []tea.Cmd
537 if p.session.ID == "" {
538 newSession, err := p.app.Sessions.Create(context.Background(), "New Session")
539 if err != nil {
540 return util.ReportError(err)
541 }
542 session = newSession
543 cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
544 }
545 _, err := p.app.CoderAgent.Run(context.Background(), session.ID, text, attachments...)
546 if err != nil {
547 return util.ReportError(err)
548 }
549 return tea.Batch(cmds...)
550}
551
552func (p *chatPage) Bindings() []key.Binding {
553 bindings := []key.Binding{
554 p.keyMap.NewSession,
555 p.keyMap.AddAttachment,
556 }
557 if p.app.CoderAgent != nil && p.app.CoderAgent.IsBusy() {
558 cancelBinding := p.keyMap.Cancel
559 if p.isCanceling {
560 cancelBinding = key.NewBinding(
561 key.WithKeys("esc"),
562 key.WithHelp("esc", "press again to cancel"),
563 )
564 }
565 bindings = append([]key.Binding{cancelBinding}, bindings...)
566 }
567
568 switch p.focusedPane {
569 case PanelTypeChat:
570 bindings = append([]key.Binding{
571 key.NewBinding(
572 key.WithKeys("tab"),
573 key.WithHelp("tab", "focus editor"),
574 ),
575 }, bindings...)
576 bindings = append(bindings, p.chat.Bindings()...)
577 case PanelTypeEditor:
578 bindings = append([]key.Binding{
579 key.NewBinding(
580 key.WithKeys("tab"),
581 key.WithHelp("tab", "focus chat"),
582 ),
583 }, bindings...)
584 bindings = append(bindings, p.editor.Bindings()...)
585 case PanelTypeSplash:
586 bindings = append(bindings, p.splash.Bindings()...)
587 }
588
589 return bindings
590}
591
592func (a *chatPage) Help() help.KeyMap {
593 var shortList []key.Binding
594 var fullList [][]key.Binding
595 switch {
596 case a.isOnboarding && !a.splash.IsShowingAPIKey():
597 shortList = append(shortList,
598 // Choose model
599 key.NewBinding(
600 key.WithKeys("up", "down"),
601 key.WithHelp("↑/↓", "choose"),
602 ),
603 // Accept selection
604 key.NewBinding(
605 key.WithKeys("enter", "ctrl+y"),
606 key.WithHelp("enter", "accept"),
607 ),
608 // Quit
609 key.NewBinding(
610 key.WithKeys("ctrl+c"),
611 key.WithHelp("ctrl+c", "quit"),
612 ),
613 )
614 // keep them the same
615 for _, v := range shortList {
616 fullList = append(fullList, []key.Binding{v})
617 }
618 case a.isOnboarding && a.splash.IsShowingAPIKey():
619 var pasteKey key.Binding
620 if runtime.GOOS != "darwin" {
621 pasteKey = key.NewBinding(
622 key.WithKeys("ctrl+v"),
623 key.WithHelp("ctrl+v", "paste API key"),
624 )
625 } else {
626 pasteKey = key.NewBinding(
627 key.WithKeys("cmd+v"),
628 key.WithHelp("cmd+v", "paste API key"),
629 )
630 }
631 shortList = append(shortList,
632 // Go back
633 key.NewBinding(
634 key.WithKeys("esc"),
635 key.WithHelp("esc", "back"),
636 ),
637 // Paste
638 pasteKey,
639 // Quit
640 key.NewBinding(
641 key.WithKeys("ctrl+c"),
642 key.WithHelp("ctrl+c", "quit"),
643 ),
644 )
645 // keep them the same
646 for _, v := range shortList {
647 fullList = append(fullList, []key.Binding{v})
648 }
649 case a.isProjectInit:
650 shortList = append(shortList,
651 key.NewBinding(
652 key.WithKeys("ctrl+c"),
653 key.WithHelp("ctrl+c", "quit"),
654 ),
655 )
656 // keep them the same
657 for _, v := range shortList {
658 fullList = append(fullList, []key.Binding{v})
659 }
660 default:
661 if a.editor.IsCompletionsOpen() {
662 shortList = append(shortList,
663 key.NewBinding(
664 key.WithKeys("tab", "enter"),
665 key.WithHelp("tab/enter", "complete"),
666 ),
667 key.NewBinding(
668 key.WithKeys("esc"),
669 key.WithHelp("esc", "cancel"),
670 ),
671 key.NewBinding(
672 key.WithKeys("up", "down"),
673 key.WithHelp("↑/↓", "choose"),
674 ),
675 )
676 for _, v := range shortList {
677 fullList = append(fullList, []key.Binding{v})
678 }
679 return core.NewSimpleHelp(shortList, fullList)
680 }
681 if a.app.CoderAgent != nil && a.app.CoderAgent.IsBusy() {
682 cancelBinding := key.NewBinding(
683 key.WithKeys("esc"),
684 key.WithHelp("esc", "cancel"),
685 )
686 if a.isCanceling {
687 cancelBinding = key.NewBinding(
688 key.WithKeys("esc"),
689 key.WithHelp("esc", "press again to cancel"),
690 )
691 }
692 shortList = append(shortList, cancelBinding)
693 fullList = append(fullList,
694 []key.Binding{
695 cancelBinding,
696 },
697 )
698 }
699 globalBindings := []key.Binding{}
700 // we are in a session
701 if a.session.ID != "" {
702 tabKey := key.NewBinding(
703 key.WithKeys("tab"),
704 key.WithHelp("tab", "focus chat"),
705 )
706 if a.focusedPane == PanelTypeChat {
707 tabKey = key.NewBinding(
708 key.WithKeys("tab"),
709 key.WithHelp("tab", "focus editor"),
710 )
711 }
712 shortList = append(shortList, tabKey)
713 globalBindings = append(globalBindings, tabKey)
714 }
715 commandsBinding := key.NewBinding(
716 key.WithKeys("ctrl+p"),
717 key.WithHelp("ctrl+p", "commands"),
718 )
719 helpBinding := key.NewBinding(
720 key.WithKeys("ctrl+g"),
721 key.WithHelp("ctrl+g", "more"),
722 )
723 globalBindings = append(globalBindings, commandsBinding)
724 globalBindings = append(globalBindings,
725 key.NewBinding(
726 key.WithKeys("ctrl+s"),
727 key.WithHelp("ctrl+s", "sessions"),
728 ),
729 )
730 if a.session.ID != "" {
731 globalBindings = append(globalBindings,
732 key.NewBinding(
733 key.WithKeys("ctrl+n"),
734 key.WithHelp("ctrl+n", "new sessions"),
735 ))
736 }
737 shortList = append(shortList,
738 // Commands
739 commandsBinding,
740 )
741 fullList = append(fullList, globalBindings)
742
743 if a.focusedPane == PanelTypeChat {
744 shortList = append(shortList,
745 key.NewBinding(
746 key.WithKeys("up", "down"),
747 key.WithHelp("↑↓", "scroll"),
748 ),
749 )
750 fullList = append(fullList,
751 []key.Binding{
752 key.NewBinding(
753 key.WithKeys("up", "down"),
754 key.WithHelp("↑↓", "scroll"),
755 ),
756 key.NewBinding(
757 key.WithKeys("shift+up", "shift+down"),
758 key.WithHelp("shift+↑↓", "next/prev item"),
759 ),
760 key.NewBinding(
761 key.WithKeys("pgup", "b"),
762 key.WithHelp("b/pgup", "page up"),
763 ),
764 key.NewBinding(
765 key.WithKeys("pgdown", " ", "f"),
766 key.WithHelp("f/pgdn", "page down"),
767 ),
768 },
769 []key.Binding{
770 key.NewBinding(
771 key.WithKeys("u"),
772 key.WithHelp("u", "half page up"),
773 ),
774 key.NewBinding(
775 key.WithKeys("d"),
776 key.WithHelp("d", "half page down"),
777 ),
778 key.NewBinding(
779 key.WithKeys("g", "home"),
780 key.WithHelp("g", "hone"),
781 ),
782 key.NewBinding(
783 key.WithKeys("G", "end"),
784 key.WithHelp("G", "end"),
785 ),
786 },
787 )
788 } else if a.focusedPane == PanelTypeEditor {
789 newLineBinding := key.NewBinding(
790 key.WithKeys("shift+enter", "ctrl+j"),
791 // "ctrl+j" is a common keybinding for newline in many editors. If
792 // the terminal supports "shift+enter", we substitute the help text
793 // to reflect that.
794 key.WithHelp("ctrl+j", "newline"),
795 )
796 if a.keyboardEnhancements.SupportsKeyDisambiguation() {
797 newLineBinding.SetHelp("shift+enter", newLineBinding.Help().Desc)
798 }
799 shortList = append(shortList, newLineBinding)
800 fullList = append(fullList,
801 []key.Binding{
802 newLineBinding,
803 key.NewBinding(
804 key.WithKeys("ctrl+f"),
805 key.WithHelp("ctrl+f", "add image"),
806 ),
807 key.NewBinding(
808 key.WithKeys("/"),
809 key.WithHelp("/", "add file"),
810 ),
811 key.NewBinding(
812 key.WithKeys("ctrl+v"),
813 key.WithHelp("ctrl+v", "open editor"),
814 ),
815 })
816 }
817 shortList = append(shortList,
818 // Quit
819 key.NewBinding(
820 key.WithKeys("ctrl+c"),
821 key.WithHelp("ctrl+c", "quit"),
822 ),
823 // Help
824 helpBinding,
825 )
826 fullList = append(fullList, []key.Binding{
827 key.NewBinding(
828 key.WithKeys("ctrl+g"),
829 key.WithHelp("ctrl+g", "less"),
830 ),
831 })
832 }
833
834 return core.NewSimpleHelp(shortList, fullList)
835}
836
837func (p *chatPage) IsChatFocused() bool {
838 return p.focusedPane == PanelTypeChat
839}