chat.go

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