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