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	cmds = append(cmds, p.chat.GoToBottom())
608	return tea.Batch(cmds...)
609}
610
611func (p *chatPage) Bindings() []key.Binding {
612	bindings := []key.Binding{
613		p.keyMap.NewSession,
614		p.keyMap.AddAttachment,
615	}
616	if p.app.CoderAgent != nil && p.app.CoderAgent.IsBusy() {
617		cancelBinding := p.keyMap.Cancel
618		if p.isCanceling {
619			cancelBinding = key.NewBinding(
620				key.WithKeys("esc"),
621				key.WithHelp("esc", "press again to cancel"),
622			)
623		}
624		bindings = append([]key.Binding{cancelBinding}, bindings...)
625	}
626
627	switch p.focusedPane {
628	case PanelTypeChat:
629		bindings = append([]key.Binding{
630			key.NewBinding(
631				key.WithKeys("tab"),
632				key.WithHelp("tab", "focus editor"),
633			),
634		}, bindings...)
635		bindings = append(bindings, p.chat.Bindings()...)
636	case PanelTypeEditor:
637		bindings = append([]key.Binding{
638			key.NewBinding(
639				key.WithKeys("tab"),
640				key.WithHelp("tab", "focus chat"),
641			),
642		}, bindings...)
643		bindings = append(bindings, p.editor.Bindings()...)
644	case PanelTypeSplash:
645		bindings = append(bindings, p.splash.Bindings()...)
646	}
647
648	return bindings
649}
650
651func (p *chatPage) Help() help.KeyMap {
652	var shortList []key.Binding
653	var fullList [][]key.Binding
654	switch {
655	case p.isOnboarding && !p.splash.IsShowingAPIKey():
656		shortList = append(shortList,
657			// Choose model
658			key.NewBinding(
659				key.WithKeys("up", "down"),
660				key.WithHelp("↑/↓", "choose"),
661			),
662			// Accept selection
663			key.NewBinding(
664				key.WithKeys("enter", "ctrl+y"),
665				key.WithHelp("enter", "accept"),
666			),
667			// Quit
668			key.NewBinding(
669				key.WithKeys("ctrl+c"),
670				key.WithHelp("ctrl+c", "quit"),
671			),
672		)
673		// keep them the same
674		for _, v := range shortList {
675			fullList = append(fullList, []key.Binding{v})
676		}
677	case p.isOnboarding && p.splash.IsShowingAPIKey():
678		if p.splash.IsAPIKeyValid() {
679			shortList = append(shortList,
680				key.NewBinding(
681					key.WithKeys("enter"),
682					key.WithHelp("enter", "continue"),
683				),
684			)
685		} else {
686			shortList = append(shortList,
687				// Go back
688				key.NewBinding(
689					key.WithKeys("esc"),
690					key.WithHelp("esc", "back"),
691				),
692			)
693		}
694		shortList = append(shortList,
695			// Quit
696			key.NewBinding(
697				key.WithKeys("ctrl+c"),
698				key.WithHelp("ctrl+c", "quit"),
699			),
700		)
701		// keep them the same
702		for _, v := range shortList {
703			fullList = append(fullList, []key.Binding{v})
704		}
705	case p.isProjectInit:
706		shortList = append(shortList,
707			key.NewBinding(
708				key.WithKeys("ctrl+c"),
709				key.WithHelp("ctrl+c", "quit"),
710			),
711		)
712		// keep them the same
713		for _, v := range shortList {
714			fullList = append(fullList, []key.Binding{v})
715		}
716	default:
717		if p.editor.IsCompletionsOpen() {
718			shortList = append(shortList,
719				key.NewBinding(
720					key.WithKeys("tab", "enter"),
721					key.WithHelp("tab/enter", "complete"),
722				),
723				key.NewBinding(
724					key.WithKeys("esc"),
725					key.WithHelp("esc", "cancel"),
726				),
727				key.NewBinding(
728					key.WithKeys("up", "down"),
729					key.WithHelp("↑/↓", "choose"),
730				),
731			)
732			for _, v := range shortList {
733				fullList = append(fullList, []key.Binding{v})
734			}
735			return core.NewSimpleHelp(shortList, fullList)
736		}
737		if p.app.CoderAgent != nil && p.app.CoderAgent.IsBusy() {
738			cancelBinding := key.NewBinding(
739				key.WithKeys("esc"),
740				key.WithHelp("esc", "cancel"),
741			)
742			if p.isCanceling {
743				cancelBinding = key.NewBinding(
744					key.WithKeys("esc"),
745					key.WithHelp("esc", "press again to cancel"),
746				)
747			}
748			shortList = append(shortList, cancelBinding)
749			fullList = append(fullList,
750				[]key.Binding{
751					cancelBinding,
752				},
753			)
754		}
755		globalBindings := []key.Binding{}
756		// we are in a session
757		if p.session.ID != "" {
758			tabKey := key.NewBinding(
759				key.WithKeys("tab"),
760				key.WithHelp("tab", "focus chat"),
761			)
762			if p.focusedPane == PanelTypeChat {
763				tabKey = key.NewBinding(
764					key.WithKeys("tab"),
765					key.WithHelp("tab", "focus editor"),
766				)
767			}
768			shortList = append(shortList, tabKey)
769			globalBindings = append(globalBindings, tabKey)
770		}
771		commandsBinding := key.NewBinding(
772			key.WithKeys("ctrl+p"),
773			key.WithHelp("ctrl+p", "commands"),
774		)
775		helpBinding := key.NewBinding(
776			key.WithKeys("ctrl+g"),
777			key.WithHelp("ctrl+g", "more"),
778		)
779		globalBindings = append(globalBindings, commandsBinding)
780		globalBindings = append(globalBindings,
781			key.NewBinding(
782				key.WithKeys("ctrl+s"),
783				key.WithHelp("ctrl+s", "sessions"),
784			),
785		)
786		if p.session.ID != "" {
787			globalBindings = append(globalBindings,
788				key.NewBinding(
789					key.WithKeys("ctrl+n"),
790					key.WithHelp("ctrl+n", "new sessions"),
791				))
792		}
793		shortList = append(shortList,
794			// Commands
795			commandsBinding,
796		)
797		fullList = append(fullList, globalBindings)
798
799		switch p.focusedPane {
800		case PanelTypeChat:
801			shortList = append(shortList,
802				key.NewBinding(
803					key.WithKeys("up", "down"),
804					key.WithHelp("↑↓", "scroll"),
805				),
806			)
807			fullList = append(fullList,
808				[]key.Binding{
809					key.NewBinding(
810						key.WithKeys("up", "down"),
811						key.WithHelp("↑↓", "scroll"),
812					),
813					key.NewBinding(
814						key.WithKeys("shift+up", "shift+down"),
815						key.WithHelp("shift+↑↓", "next/prev item"),
816					),
817					key.NewBinding(
818						key.WithKeys("pgup", "b"),
819						key.WithHelp("b/pgup", "page up"),
820					),
821					key.NewBinding(
822						key.WithKeys("pgdown", " ", "f"),
823						key.WithHelp("f/pgdn", "page down"),
824					),
825				},
826				[]key.Binding{
827					key.NewBinding(
828						key.WithKeys("u"),
829						key.WithHelp("u", "half page up"),
830					),
831					key.NewBinding(
832						key.WithKeys("d"),
833						key.WithHelp("d", "half page down"),
834					),
835					key.NewBinding(
836						key.WithKeys("g", "home"),
837						key.WithHelp("g", "hone"),
838					),
839					key.NewBinding(
840						key.WithKeys("G", "end"),
841						key.WithHelp("G", "end"),
842					),
843				},
844			)
845		case PanelTypeEditor:
846			newLineBinding := key.NewBinding(
847				key.WithKeys("shift+enter", "ctrl+j"),
848				// "ctrl+j" is a common keybinding for newline in many editors. If
849				// the terminal supports "shift+enter", we substitute the help text
850				// to reflect that.
851				key.WithHelp("ctrl+j", "newline"),
852			)
853			if p.keyboardEnhancements.SupportsKeyDisambiguation() {
854				newLineBinding.SetHelp("shift+enter", newLineBinding.Help().Desc)
855			}
856			shortList = append(shortList, newLineBinding)
857			fullList = append(fullList,
858				[]key.Binding{
859					newLineBinding,
860					key.NewBinding(
861						key.WithKeys("ctrl+f"),
862						key.WithHelp("ctrl+f", "add image"),
863					),
864					key.NewBinding(
865						key.WithKeys("/"),
866						key.WithHelp("/", "add file"),
867					),
868					key.NewBinding(
869						key.WithKeys("ctrl+v"),
870						key.WithHelp("ctrl+v", "open editor"),
871					),
872					key.NewBinding(
873						key.WithKeys("ctrl+r"),
874						key.WithHelp("ctrl+r+{i}", "delete attachment at index i"),
875					),
876					key.NewBinding(
877						key.WithKeys("ctrl+r", "r"),
878						key.WithHelp("ctrl+r+r", "delete all attachments"),
879					),
880					key.NewBinding(
881						key.WithKeys("esc"),
882						key.WithHelp("esc", "cancel delete mode"),
883					),
884				})
885		}
886		shortList = append(shortList,
887			// Quit
888			key.NewBinding(
889				key.WithKeys("ctrl+c"),
890				key.WithHelp("ctrl+c", "quit"),
891			),
892			// Help
893			helpBinding,
894		)
895		fullList = append(fullList, []key.Binding{
896			key.NewBinding(
897				key.WithKeys("ctrl+g"),
898				key.WithHelp("ctrl+g", "less"),
899			),
900		})
901	}
902
903	return core.NewSimpleHelp(shortList, fullList)
904}
905
906func (p *chatPage) IsChatFocused() bool {
907	return p.focusedPane == PanelTypeChat
908}