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