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