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