@@ -2,8 +2,10 @@ package chat
22
33import (
44 "context"
5+ "runtime"
56 "time"
67
8+ "github.com/charmbracelet/bubbles/v2/help"
79 "github.com/charmbracelet/bubbles/v2/key"
810 "github.com/charmbracelet/bubbles/v2/spinner"
911 tea "github.com/charmbracelet/bubbletea/v2"
@@ -20,6 +22,7 @@ import (
2022 "github.com/charmbracelet/crush/internal/tui/components/chat/sidebar"
2123 "github.com/charmbracelet/crush/internal/tui/components/chat/splash"
2224 "github.com/charmbracelet/crush/internal/tui/components/completions"
25+ "github.com/charmbracelet/crush/internal/tui/components/core"
2326 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
2427 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
2528 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
@@ -82,6 +85,7 @@ type chatPage struct {
8285 width, height int
8386 detailsWidth, detailsHeight int
8487 app *app.App
88+ keyboardEnhancements tea.KeyboardEnhancementsMsg
8589
8690 // Layout state
8791 compact bool
@@ -103,6 +107,8 @@ type chatPage struct {
103107 showingDetails bool
104108 isCanceling bool
105109 splashFullScreen bool
110+ isOnboarding bool
111+ isProjectInit bool
106112}
107113
108114func New(app *app.App) ChatPage {
@@ -129,10 +135,12 @@ func (p *chatPage) Init() tea.Cmd {
129135 if !config.HasInitialDataConfig() {
130136 // First-time setup: show model selection
131137 p.splash.SetOnboarding(true)
138+ p.isOnboarding = true
132139 p.splashFullScreen = true
133140 } else if b, _ := config.ProjectNeedsInitialization(); b {
134141 // Project needs CRUSH.md initialization
135142 p.splash.SetProjectInit(true)
143+ p.isProjectInit = true
136144 p.splashFullScreen = true
137145 } else {
138146 // Ready to chat: focus editor, splash in background
@@ -153,9 +161,8 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
153161 var cmds []tea.Cmd
154162 switch msg := msg.(type) {
155163 case tea.KeyboardEnhancementsMsg:
156− m, cmd := p.editor.Update(msg)
157− p.editor = m.(editor.Editor)
158− return p, cmd
164+ p.keyboardEnhancements = msg
165+ return p, nil
159166 case tea.WindowSizeMsg:
160167 return p, p.SetSize(msg.Width, msg.Height)
161168 case CancelTimerExpiredMsg:
@@ -237,6 +244,8 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
237244 if err != nil {
238245 return p, util.ReportError(err)
239246 }
247+ p.isOnboarding = false
248+ p.isProjectInit = false
240249 p.focusedPane = PanelTypeEditor
241250 return p, p.SetSize(p.width, p.height)
242251 case tea.KeyPressMsg:
@@ -579,6 +588,231 @@ func (p *chatPage) Bindings() []key.Binding {
579588 return bindings
580589}
581590
591+func (a *chatPage) Help() help.KeyMap {
592+ var shortList []key.Binding
593+ var fullList [][]key.Binding
594+ switch {
595+ case a.isOnboarding && !a.splash.IsShowingAPIKey():
596+ shortList = append(shortList,
597+ // Choose model
598+ key.NewBinding(
599+ key.WithKeys("up", "down"),
600+ key.WithHelp("↑/↓", "choose"),
601+ ),
602+ // Accept selection
603+ key.NewBinding(
604+ key.WithKeys("enter", "ctrl+y"),
605+ key.WithHelp("enter", "accept"),
606+ ),
607+ // Quit
608+ key.NewBinding(
609+ key.WithKeys("ctrl+c"),
610+ key.WithHelp("ctrl+c", "quit"),
611+ ),
612+ )
613+ // keep them the same
614+ for _, v := range shortList {
615+ fullList = append(fullList, []key.Binding{v})
616+ }
617+ case a.isOnboarding && a.splash.IsShowingAPIKey():
618+ var pasteKey key.Binding
619+ if runtime.GOOS != "darwin" {
620+ pasteKey = key.NewBinding(
621+ key.WithKeys("ctrl+v"),
622+ key.WithHelp("ctrl+v", "paste API key"),
623+ )
624+ } else {
625+ pasteKey = key.NewBinding(
626+ key.WithKeys("cmd+v"),
627+ key.WithHelp("cmd+v", "paste API key"),
628+ )
629+ }
630+ shortList = append(shortList,
631+ // Go back
632+ key.NewBinding(
633+ key.WithKeys("esc"),
634+ key.WithHelp("esc", "back"),
635+ ),
636+ // Paste
637+ pasteKey,
638+ // Quit
639+ key.NewBinding(
640+ key.WithKeys("ctrl+c"),
641+ key.WithHelp("ctrl+c", "quit"),
642+ ),
643+ )
644+ // keep them the same
645+ for _, v := range shortList {
646+ fullList = append(fullList, []key.Binding{v})
647+ }
648+ case a.isProjectInit:
649+ shortList = append(shortList,
650+ key.NewBinding(
651+ key.WithKeys("ctrl+c"),
652+ key.WithHelp("ctrl+c", "quit"),
653+ ),
654+ )
655+ // keep them the same
656+ for _, v := range shortList {
657+ fullList = append(fullList, []key.Binding{v})
658+ }
659+ default:
660+ if a.app.CoderAgent != nil && a.app.CoderAgent.IsBusy() {
661+ cancelBinding := key.NewBinding(
662+ key.WithKeys("esc"),
663+ key.WithHelp("esc", "cancel"),
664+ )
665+ if a.isCanceling {
666+ cancelBinding = key.NewBinding(
667+ key.WithKeys("esc"),
668+ key.WithHelp("esc", "press again to cancel"),
669+ )
670+ }
671+ shortList = append(shortList, cancelBinding)
672+ fullList = append(fullList,
673+ []key.Binding{
674+ cancelBinding,
675+ },
676+ )
677+ }
678+ globalBindings := []key.Binding{}
679+ // we are in a session
680+ if a.session.ID != "" {
681+ tabKey := key.NewBinding(
682+ key.WithKeys("tab"),
683+ key.WithHelp("tab", "focus chat"),
684+ )
685+ if a.focusedPane == PanelTypeChat {
686+ tabKey = key.NewBinding(
687+ key.WithKeys("tab"),
688+ key.WithHelp("tab", "focus editor"),
689+ )
690+ }
691+ shortList = append(shortList, tabKey)
692+ globalBindings = append(globalBindings, tabKey)
693+ }
694+ commandsBinding := key.NewBinding(
695+ key.WithKeys("ctrl+p"),
696+ key.WithHelp("ctrl+p", "commands"),
697+ )
698+ helpBinding := key.NewBinding(
699+ key.WithKeys("ctrl+g"),
700+ key.WithHelp("ctrl+g", "more"),
701+ )
702+ globalBindings = append(globalBindings, commandsBinding)
703+ globalBindings = append(globalBindings,
704+ key.NewBinding(
705+ key.WithKeys("ctrl+s"),
706+ key.WithHelp("ctrl+s", "sessions"),
707+ ),
708+ )
709+ if a.session.ID != "" {
710+ globalBindings = append(globalBindings,
711+ key.NewBinding(
712+ key.WithKeys("ctrl+n"),
713+ key.WithHelp("ctrl+n", "new sessions"),
714+ ))
715+ }
716+ shortList = append(shortList,
717+ // Commands
718+ commandsBinding,
719+ )
720+ fullList = append(fullList, globalBindings)
721+
722+ if a.focusedPane == PanelTypeChat {
723+ shortList = append(shortList,
724+ key.NewBinding(
725+ key.WithKeys("up", "down"),
726+ key.WithHelp("↑↓", "scroll"),
727+ ),
728+ )
729+ fullList = append(fullList,
730+ []key.Binding{
731+ key.NewBinding(
732+ key.WithKeys("up", "down"),
733+ key.WithHelp("↑↓", "scroll"),
734+ ),
735+ key.NewBinding(
736+ key.WithKeys("shift+up", "shift+down"),
737+ key.WithHelp("shift+↑↓", "next/prev item"),
738+ ),
739+ key.NewBinding(
740+ key.WithKeys("pgup", "b"),
741+ key.WithHelp("b/pgup", "page up"),
742+ ),
743+ key.NewBinding(
744+ key.WithKeys("pgdown", " ", "f"),
745+ key.WithHelp("f/pgdn", "page down"),
746+ ),
747+ },
748+ []key.Binding{
749+ key.NewBinding(
750+ key.WithKeys("u"),
751+ key.WithHelp("u", "half page up"),
752+ ),
753+ key.NewBinding(
754+ key.WithKeys("d"),
755+ key.WithHelp("d", "half page down"),
756+ ),
757+ key.NewBinding(
758+ key.WithKeys("g", "home"),
759+ key.WithHelp("g", "hone"),
760+ ),
761+ key.NewBinding(
762+ key.WithKeys("G", "end"),
763+ key.WithHelp("G", "end"),
764+ ),
765+ },
766+ )
767+ } else if a.focusedPane == PanelTypeEditor {
768+ newLineBinding := key.NewBinding(
769+ key.WithKeys("shift+enter", "ctrl+j"),
770+ // "ctrl+j" is a common keybinding for newline in many editors. If
771+ // the terminal supports "shift+enter", we substitute the help text
772+ // to reflect that.
773+ key.WithHelp("ctrl+j", "newline"),
774+ )
775+ if a.keyboardEnhancements.SupportsKeyDisambiguation() {
776+ newLineBinding.SetHelp("shift+enter", newLineBinding.Help().Desc)
777+ }
778+ shortList = append(shortList, newLineBinding)
779+ fullList = append(fullList,
780+ []key.Binding{
781+ newLineBinding,
782+ key.NewBinding(
783+ key.WithKeys("ctrl+f"),
784+ key.WithHelp("ctrl+f", "add image"),
785+ ),
786+ key.NewBinding(
787+ key.WithKeys("/"),
788+ key.WithHelp("/", "add file"),
789+ ),
790+ key.NewBinding(
791+ key.WithKeys("ctrl+e"),
792+ key.WithHelp("ctrl+e", "open editor"),
793+ ),
794+ })
795+ }
796+ shortList = append(shortList,
797+ // Quit
798+ key.NewBinding(
799+ key.WithKeys("ctrl+c"),
800+ key.WithHelp("ctrl+c", "quit"),
801+ ),
802+ // Help
803+ helpBinding,
804+ )
805+ fullList = append(fullList, []key.Binding{
806+ key.NewBinding(
807+ key.WithKeys("ctrl+g"),
808+ key.WithHelp("ctrl+g", "less"),
809+ ),
810+ })
811+ }
812+
813+ return core.NewSimpleHelp(shortList, fullList)
814+}
815+
582816func (p *chatPage) IsChatFocused() bool {
583817 return p.focusedPane == PanelTypeChat
584818}