9 "charm.land/bubbles/v2/help"
10 "charm.land/bubbles/v2/key"
11 "charm.land/bubbles/v2/spinner"
12 tea "charm.land/bubbletea/v2"
13 "charm.land/lipgloss/v2"
14 "github.com/charmbracelet/crush/internal/app"
15 "github.com/charmbracelet/crush/internal/config"
16 "github.com/charmbracelet/crush/internal/history"
17 "github.com/charmbracelet/crush/internal/message"
18 "github.com/charmbracelet/crush/internal/permission"
19 "github.com/charmbracelet/crush/internal/pubsub"
20 "github.com/charmbracelet/crush/internal/session"
21 "github.com/charmbracelet/crush/internal/tui/components/anim"
22 "github.com/charmbracelet/crush/internal/tui/components/chat"
23 "github.com/charmbracelet/crush/internal/tui/components/chat/editor"
24 "github.com/charmbracelet/crush/internal/tui/components/chat/header"
25 "github.com/charmbracelet/crush/internal/tui/components/chat/messages"
26 "github.com/charmbracelet/crush/internal/tui/components/chat/sidebar"
27 "github.com/charmbracelet/crush/internal/tui/components/chat/splash"
28 "github.com/charmbracelet/crush/internal/tui/components/completions"
29 "github.com/charmbracelet/crush/internal/tui/components/core"
30 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
31 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
32 "github.com/charmbracelet/crush/internal/tui/components/dialogs/claude"
33 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
34 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
35 "github.com/charmbracelet/crush/internal/tui/components/dialogs/models"
36 "github.com/charmbracelet/crush/internal/tui/components/dialogs/reasoning"
37 "github.com/charmbracelet/crush/internal/tui/page"
38 "github.com/charmbracelet/crush/internal/tui/styles"
39 "github.com/charmbracelet/crush/internal/tui/util"
40 "github.com/charmbracelet/crush/internal/version"
43var ChatPageID page.PageID = "chat"
46 ChatFocusedMsg struct {
49 CancelTimerExpiredMsg struct{}
55 PanelTypeChat PanelType = "chat"
56 PanelTypeEditor PanelType = "editor"
57 PanelTypeSplash PanelType = "splash"
61 CompactModeWidthBreakpoint = 120 // Width at which the chat page switches to compact mode
62 CompactModeHeightBreakpoint = 30 // Height at which the chat page switches to compact mode
63 EditorHeight = 5 // Height of the editor input area including padding
64 SideBarWidth = 31 // Width of the sidebar
65 SideBarDetailsPadding = 1 // Padding for the sidebar details section
66 HeaderHeight = 1 // Height of the header
68 // Layout constants for borders and padding
69 BorderWidth = 1 // Width of component borders
70 LeftRightBorders = 2 // Left + right border width (1 + 1)
71 TopBottomBorders = 2 // Top + bottom border width (1 + 1)
72 DetailsPositioning = 2 // Positioning adjustment for details panel
75 CancelTimerDuration = 2 * time.Second // Duration before cancel timer expires
78type ChatPage interface {
84// cancelTimerCmd creates a command that expires the cancel timer
85func cancelTimerCmd() tea.Cmd {
86 return tea.Tick(CancelTimerDuration, func(time.Time) tea.Msg {
87 return CancelTimerExpiredMsg{}
93 detailsWidth, detailsHeight int
95 keyboardEnhancements tea.KeyboardEnhancementsMsg
100 focusedPane PanelType
103 session session.Session
108 sidebar sidebar.Sidebar
109 chat chat.MessageListCmp
113 // Simple state flags
116 splashFullScreen bool
121func New(app *app.App) ChatPage {
124 keyMap: DefaultKeyMap(),
125 header: header.New(app.LSPClients),
126 sidebar: sidebar.New(app.History, app.LSPClients, false),
128 editor: editor.New(app),
129 splash: splash.New(),
130 focusedPane: PanelTypeSplash,
134func (p *chatPage) Init() tea.Cmd {
136 compact := cfg.Options.TUI.CompactMode
138 p.forceCompact = compact
139 p.sidebar.SetCompactMode(p.compact)
141 // Set splash state based on config
142 if !config.HasInitialDataConfig() {
143 // First-time setup: show model selection
144 p.splash.SetOnboarding(true)
145 p.isOnboarding = true
146 p.splashFullScreen = true
147 } else if b, _ := config.ProjectNeedsInitialization(); b {
148 // Project needs context initialization
149 p.splash.SetProjectInit(true)
150 p.isProjectInit = true
151 p.splashFullScreen = true
153 // Ready to chat: focus editor, splash in background
154 p.focusedPane = PanelTypeEditor
155 p.splashFullScreen = false
167func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) {
169 switch msg := msg.(type) {
170 case tea.KeyboardEnhancementsMsg:
171 p.keyboardEnhancements = msg
173 case tea.MouseWheelMsg:
177 if p.isMouseOverChat(msg.X, msg.Y) {
178 u, cmd := p.chat.Update(msg)
179 p.chat = u.(chat.MessageListCmp)
183 case tea.MouseClickMsg:
184 if p.isOnboarding || p.isProjectInit {
190 if p.isMouseOverChat(msg.X, msg.Y) {
191 p.focusedPane = PanelTypeChat
195 p.focusedPane = PanelTypeEditor
199 u, cmd := p.chat.Update(msg)
200 p.chat = u.(chat.MessageListCmp)
202 case tea.MouseMotionMsg:
206 if msg.Button == tea.MouseLeft {
207 u, cmd := p.chat.Update(msg)
208 p.chat = u.(chat.MessageListCmp)
212 case tea.MouseReleaseMsg:
213 if p.isOnboarding || p.isProjectInit {
219 if msg.Button == tea.MouseLeft {
220 u, cmd := p.chat.Update(msg)
221 p.chat = u.(chat.MessageListCmp)
225 case chat.SelectionCopyMsg:
226 u, cmd := p.chat.Update(msg)
227 p.chat = u.(chat.MessageListCmp)
229 case tea.WindowSizeMsg:
230 u, cmd := p.editor.Update(msg)
231 p.editor = u.(editor.Editor)
232 return p, tea.Batch(p.SetSize(msg.Width, msg.Height), cmd)
233 case CancelTimerExpiredMsg:
234 p.isCanceling = false
236 case editor.OpenEditorMsg:
237 u, cmd := p.editor.Update(msg)
238 p.editor = u.(editor.Editor)
241 return p, p.sendMessage(msg.Text, msg.Attachments)
242 case chat.SessionSelectedMsg:
243 return p, p.setSession(msg)
244 case splash.SubmitAPIKeyMsg:
245 u, cmd := p.splash.Update(msg)
246 p.splash = u.(splash.Splash)
247 cmds = append(cmds, cmd)
248 return p, tea.Batch(cmds...)
249 case commands.ToggleCompactModeMsg:
250 p.forceCompact = !p.forceCompact
253 p.setCompactMode(true)
254 cmd = p.updateCompactConfig(true)
255 } else if p.width >= CompactModeWidthBreakpoint && p.height >= CompactModeHeightBreakpoint {
256 p.setCompactMode(false)
257 cmd = p.updateCompactConfig(false)
259 return p, tea.Batch(p.SetSize(p.width, p.height), cmd)
260 case commands.ToggleThinkingMsg:
261 return p, p.toggleThinking()
262 case commands.OpenReasoningDialogMsg:
263 return p, p.openReasoningDialog()
264 case reasoning.ReasoningEffortSelectedMsg:
265 return p, p.handleReasoningEffortSelected(msg.Effort)
266 case commands.OpenExternalEditorMsg:
267 u, cmd := p.editor.Update(msg)
268 p.editor = u.(editor.Editor)
270 case pubsub.Event[session.Session]:
271 u, cmd := p.header.Update(msg)
272 p.header = u.(header.Header)
273 cmds = append(cmds, cmd)
274 u, cmd = p.sidebar.Update(msg)
275 p.sidebar = u.(sidebar.Sidebar)
276 cmds = append(cmds, cmd)
277 return p, tea.Batch(cmds...)
278 case chat.SessionClearedMsg:
279 u, cmd := p.header.Update(msg)
280 p.header = u.(header.Header)
281 cmds = append(cmds, cmd)
282 u, cmd = p.sidebar.Update(msg)
283 p.sidebar = u.(sidebar.Sidebar)
284 cmds = append(cmds, cmd)
285 u, cmd = p.chat.Update(msg)
286 p.chat = u.(chat.MessageListCmp)
287 cmds = append(cmds, cmd)
288 return p, tea.Batch(cmds...)
289 case filepicker.FilePickedMsg,
290 completions.CompletionsClosedMsg,
291 completions.SelectCompletionMsg:
292 u, cmd := p.editor.Update(msg)
293 p.editor = u.(editor.Editor)
294 cmds = append(cmds, cmd)
295 return p, tea.Batch(cmds...)
297 case claude.ValidationCompletedMsg, claude.AuthenticationCompleteMsg:
298 if p.focusedPane == PanelTypeSplash {
299 u, cmd := p.splash.Update(msg)
300 p.splash = u.(splash.Splash)
301 cmds = append(cmds, cmd)
303 return p, tea.Batch(cmds...)
304 case models.APIKeyStateChangeMsg:
305 if p.focusedPane == PanelTypeSplash {
306 u, cmd := p.splash.Update(msg)
307 p.splash = u.(splash.Splash)
308 cmds = append(cmds, cmd)
310 return p, tea.Batch(cmds...)
311 case pubsub.Event[message.Message],
314 if p.focusedPane == PanelTypeSplash {
315 u, cmd := p.splash.Update(msg)
316 p.splash = u.(splash.Splash)
317 cmds = append(cmds, cmd)
319 u, cmd := p.chat.Update(msg)
320 p.chat = u.(chat.MessageListCmp)
321 cmds = append(cmds, cmd)
324 return p, tea.Batch(cmds...)
325 case commands.ToggleYoloModeMsg:
326 // update the editor style
327 u, cmd := p.editor.Update(msg)
328 p.editor = u.(editor.Editor)
330 case pubsub.Event[history.File], sidebar.SessionFilesMsg:
331 u, cmd := p.sidebar.Update(msg)
332 p.sidebar = u.(sidebar.Sidebar)
333 cmds = append(cmds, cmd)
334 return p, tea.Batch(cmds...)
335 case pubsub.Event[permission.PermissionNotification]:
336 u, cmd := p.chat.Update(msg)
337 p.chat = u.(chat.MessageListCmp)
338 cmds = append(cmds, cmd)
339 return p, tea.Batch(cmds...)
341 case commands.CommandRunCustomMsg:
342 if p.app.AgentCoordinator.IsBusy() {
343 return p, util.ReportWarn("Agent is busy, please wait before executing a command...")
346 cmd := p.sendMessage(msg.Content, nil)
350 case splash.OnboardingCompleteMsg:
351 p.splashFullScreen = false
352 if b, _ := config.ProjectNeedsInitialization(); b {
353 p.splash.SetProjectInit(true)
354 p.splashFullScreen = true
355 return p, p.SetSize(p.width, p.height)
357 err := p.app.InitCoderAgent(context.TODO())
359 return p, util.ReportError(err)
361 p.isOnboarding = false
362 p.isProjectInit = false
363 p.focusedPane = PanelTypeEditor
364 return p, p.SetSize(p.width, p.height)
365 case commands.NewSessionsMsg:
366 if p.app.AgentCoordinator.IsBusy() {
367 return p, util.ReportWarn("Agent is busy, please wait before starting a new session...")
369 return p, p.newSession()
370 case tea.KeyPressMsg:
372 case key.Matches(msg, p.keyMap.NewSession):
373 // if we have no agent do nothing
374 if p.app.AgentCoordinator == nil {
377 if p.app.AgentCoordinator.IsBusy() {
378 return p, util.ReportWarn("Agent is busy, please wait before starting a new session...")
380 return p, p.newSession()
381 case key.Matches(msg, p.keyMap.AddAttachment):
382 // Skip attachment handling during onboarding/splash screen
383 if p.focusedPane == PanelTypeSplash || p.isOnboarding {
384 u, cmd := p.splash.Update(msg)
385 p.splash = u.(splash.Splash)
388 agentCfg := config.Get().Agents[config.AgentCoder]
389 model := config.Get().GetModelByType(agentCfg.Model)
391 return p, util.ReportWarn("No model configured yet")
393 if model.SupportsImages {
394 return p, util.CmdHandler(commands.OpenFilePickerMsg{})
396 return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Name)
398 case key.Matches(msg, p.keyMap.Tab):
399 if p.session.ID == "" {
400 u, cmd := p.splash.Update(msg)
401 p.splash = u.(splash.Splash)
406 case key.Matches(msg, p.keyMap.Cancel):
407 if p.session.ID != "" && p.app.AgentCoordinator.IsBusy() {
410 case key.Matches(msg, p.keyMap.Details):
415 switch p.focusedPane {
417 u, cmd := p.chat.Update(msg)
418 p.chat = u.(chat.MessageListCmp)
419 cmds = append(cmds, cmd)
420 case PanelTypeEditor:
421 u, cmd := p.editor.Update(msg)
422 p.editor = u.(editor.Editor)
423 cmds = append(cmds, cmd)
424 case PanelTypeSplash:
425 u, cmd := p.splash.Update(msg)
426 p.splash = u.(splash.Splash)
427 cmds = append(cmds, cmd)
430 switch p.focusedPane {
431 case PanelTypeEditor:
432 u, cmd := p.editor.Update(msg)
433 p.editor = u.(editor.Editor)
434 cmds = append(cmds, cmd)
435 return p, tea.Batch(cmds...)
437 u, cmd := p.chat.Update(msg)
438 p.chat = u.(chat.MessageListCmp)
439 cmds = append(cmds, cmd)
440 return p, tea.Batch(cmds...)
441 case PanelTypeSplash:
442 u, cmd := p.splash.Update(msg)
443 p.splash = u.(splash.Splash)
444 cmds = append(cmds, cmd)
445 return p, tea.Batch(cmds...)
448 return p, tea.Batch(cmds...)
451func (p *chatPage) Cursor() *tea.Cursor {
452 if p.header.ShowingDetails() {
455 switch p.focusedPane {
456 case PanelTypeEditor:
457 return p.editor.Cursor()
458 case PanelTypeSplash:
459 return p.splash.Cursor()
465func (p *chatPage) View() string {
467 t := styles.CurrentTheme()
469 if p.session.ID == "" {
470 splashView := p.splash.View()
471 // Full screen during onboarding or project initialization
472 if p.splashFullScreen {
473 chatView = splashView
475 // Show splash + editor for new message state
476 editorView := p.editor.View()
477 chatView = lipgloss.JoinVertical(
479 t.S().Base.Render(splashView),
484 messagesView := p.chat.View()
485 editorView := p.editor.View()
487 headerView := p.header.View()
488 chatView = lipgloss.JoinVertical(
495 sidebarView := p.sidebar.View()
496 messages := lipgloss.JoinHorizontal(
501 chatView = lipgloss.JoinVertical(
509 layers := []*lipgloss.Layer{
510 lipgloss.NewLayer(chatView).X(0).Y(0),
513 if p.showingDetails {
515 Width(p.detailsWidth).
516 Border(lipgloss.RoundedBorder()).
517 BorderForeground(t.BorderFocus)
518 version := t.S().Base.Foreground(t.Border).Width(p.detailsWidth - 4).AlignHorizontal(lipgloss.Right).Render(version.Version)
519 details := style.Render(
520 lipgloss.JoinVertical(
526 layers = append(layers, lipgloss.NewLayer(details).X(1).Y(1))
528 canvas := lipgloss.NewCanvas(
531 return canvas.Render()
534func (p *chatPage) updateCompactConfig(compact bool) tea.Cmd {
535 return func() tea.Msg {
536 err := config.Get().SetCompactMode(compact)
539 Type: util.InfoTypeError,
540 Msg: "Failed to update compact mode configuration: " + err.Error(),
547func (p *chatPage) toggleThinking() tea.Cmd {
548 return func() tea.Msg {
550 agentCfg := cfg.Agents[config.AgentCoder]
551 currentModel := cfg.Models[agentCfg.Model]
553 // Toggle the thinking mode
554 currentModel.Think = !currentModel.Think
555 if err := cfg.UpdatePreferredModel(agentCfg.Model, currentModel); err != nil {
557 Type: util.InfoTypeError,
558 Msg: "Failed to update thinking mode: " + err.Error(),
562 // Update the agent with the new configuration
563 go p.app.UpdateAgentModel(context.TODO())
566 if currentModel.Think {
570 Type: util.InfoTypeInfo,
571 Msg: "Thinking mode " + status,
576func (p *chatPage) openReasoningDialog() tea.Cmd {
577 return func() tea.Msg {
579 agentCfg := cfg.Agents[config.AgentCoder]
580 model := cfg.GetModelByType(agentCfg.Model)
581 providerCfg := cfg.GetProviderForModel(agentCfg.Model)
583 if providerCfg != nil && model != nil && len(model.ReasoningLevels) > 0 {
584 // Return the OpenDialogMsg directly so it bubbles up to the main TUI
585 return dialogs.OpenDialogMsg{
586 Model: reasoning.NewReasoningDialog(),
593func (p *chatPage) handleReasoningEffortSelected(effort string) tea.Cmd {
594 return func() tea.Msg {
596 agentCfg := cfg.Agents[config.AgentCoder]
597 currentModel := cfg.Models[agentCfg.Model]
599 // Update the model configuration
600 currentModel.ReasoningEffort = effort
601 if err := cfg.UpdatePreferredModel(agentCfg.Model, currentModel); err != nil {
603 Type: util.InfoTypeError,
604 Msg: "Failed to update reasoning effort: " + err.Error(),
608 // Update the agent with the new configuration
609 if err := p.app.UpdateAgentModel(context.TODO()); err != nil {
611 Type: util.InfoTypeError,
612 Msg: "Failed to update reasoning effort: " + err.Error(),
617 Type: util.InfoTypeInfo,
618 Msg: "Reasoning effort set to " + effort,
623func (p *chatPage) setCompactMode(compact bool) {
624 if p.compact == compact {
629 p.sidebar.SetCompactMode(true)
631 p.setShowDetails(false)
635func (p *chatPage) handleCompactMode(newWidth int, newHeight int) {
639 if (newWidth < CompactModeWidthBreakpoint || newHeight < CompactModeHeightBreakpoint) && !p.compact {
640 p.setCompactMode(true)
642 if (newWidth >= CompactModeWidthBreakpoint && newHeight >= CompactModeHeightBreakpoint) && p.compact {
643 p.setCompactMode(false)
647func (p *chatPage) SetSize(width, height int) tea.Cmd {
648 p.handleCompactMode(width, height)
653 if p.session.ID == "" {
654 if p.splashFullScreen {
655 cmds = append(cmds, p.splash.SetSize(width, height))
657 cmds = append(cmds, p.splash.SetSize(width, height-EditorHeight))
658 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
659 cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
663 cmds = append(cmds, p.chat.SetSize(width, height-EditorHeight-HeaderHeight))
664 p.detailsWidth = width - DetailsPositioning
665 cmds = append(cmds, p.sidebar.SetSize(p.detailsWidth-LeftRightBorders, p.detailsHeight-TopBottomBorders))
666 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
667 cmds = append(cmds, p.header.SetWidth(width-BorderWidth))
669 cmds = append(cmds, p.chat.SetSize(width-SideBarWidth, height-EditorHeight))
670 cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
671 cmds = append(cmds, p.sidebar.SetSize(SideBarWidth, height-EditorHeight))
673 cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
675 return tea.Batch(cmds...)
678func (p *chatPage) newSession() tea.Cmd {
679 if p.session.ID == "" {
683 p.session = session.Session{}
684 p.focusedPane = PanelTypeEditor
687 p.isCanceling = false
689 util.CmdHandler(chat.SessionClearedMsg{}),
690 p.SetSize(p.width, p.height),
694func (p *chatPage) setSession(session session.Session) tea.Cmd {
695 if p.session.ID == session.ID {
702 cmds = append(cmds, p.SetSize(p.width, p.height))
703 cmds = append(cmds, p.chat.SetSession(session))
704 cmds = append(cmds, p.sidebar.SetSession(session))
705 cmds = append(cmds, p.header.SetSession(session))
706 cmds = append(cmds, p.editor.SetSession(session))
708 return tea.Sequence(cmds...)
711func (p *chatPage) changeFocus() {
712 if p.session.ID == "" {
715 switch p.focusedPane {
717 p.focusedPane = PanelTypeEditor
720 case PanelTypeEditor:
721 p.focusedPane = PanelTypeChat
727func (p *chatPage) cancel() tea.Cmd {
729 p.isCanceling = false
730 if p.app.AgentCoordinator != nil {
731 p.app.AgentCoordinator.Cancel(p.session.ID)
736 if p.app.AgentCoordinator != nil && p.app.AgentCoordinator.QueuedPrompts(p.session.ID) > 0 {
737 p.app.AgentCoordinator.ClearQueue(p.session.ID)
741 return cancelTimerCmd()
744func (p *chatPage) setShowDetails(show bool) {
745 p.showingDetails = show
746 p.header.SetDetailsOpen(p.showingDetails)
748 p.sidebar.SetCompactMode(false)
752func (p *chatPage) toggleDetails() {
753 if p.session.ID == "" || !p.compact {
756 p.setShowDetails(!p.showingDetails)
759func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
762 if p.session.ID == "" {
763 newSession, err := p.app.Sessions.Create(context.Background(), "New Session")
765 return util.ReportError(err)
768 cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
770 if p.app.AgentCoordinator == nil {
771 return util.ReportError(fmt.Errorf("coder agent is not initialized"))
773 cmds = append(cmds, p.chat.GoToBottom())
774 cmds = append(cmds, func() tea.Msg {
775 _, err := p.app.AgentCoordinator.Run(context.Background(), session.ID, text, attachments...)
777 isCancelErr := errors.Is(err, context.Canceled)
778 isPermissionErr := errors.Is(err, permission.ErrorPermissionDenied)
779 if isCancelErr || isPermissionErr {
783 Type: util.InfoTypeError,
789 return tea.Batch(cmds...)
792func (p *chatPage) Bindings() []key.Binding {
793 bindings := []key.Binding{
795 p.keyMap.AddAttachment,
797 if p.app.AgentCoordinator != nil && p.app.AgentCoordinator.IsBusy() {
798 cancelBinding := p.keyMap.Cancel
800 cancelBinding = key.NewBinding(
801 key.WithKeys("esc", "alt+esc"),
802 key.WithHelp("esc", "press again to cancel"),
805 bindings = append([]key.Binding{cancelBinding}, bindings...)
808 switch p.focusedPane {
810 bindings = append([]key.Binding{
813 key.WithHelp("tab", "focus editor"),
816 bindings = append(bindings, p.chat.Bindings()...)
817 case PanelTypeEditor:
818 bindings = append([]key.Binding{
821 key.WithHelp("tab", "focus chat"),
824 bindings = append(bindings, p.editor.Bindings()...)
825 case PanelTypeSplash:
826 bindings = append(bindings, p.splash.Bindings()...)
832func (p *chatPage) Help() help.KeyMap {
833 var shortList []key.Binding
834 var fullList [][]key.Binding
836 case p.isOnboarding && p.splash.IsShowingClaudeAuthMethodChooser():
837 shortList = append(shortList,
838 // Choose auth method
840 key.WithKeys("left", "right", "tab"),
841 key.WithHelp("←→/tab", "choose"),
845 key.WithKeys("enter"),
846 key.WithHelp("enter", "accept"),
850 key.WithKeys("esc", "alt+esc"),
851 key.WithHelp("esc", "back"),
855 key.WithKeys("ctrl+c"),
856 key.WithHelp("ctrl+c", "quit"),
859 // keep them the same
860 for _, v := range shortList {
861 fullList = append(fullList, []key.Binding{v})
863 case p.isOnboarding && p.splash.IsShowingClaudeOAuth2():
864 if p.splash.IsClaudeOAuthURLState() {
865 shortList = append(shortList,
867 key.WithKeys("enter"),
868 key.WithHelp("enter", "open"),
872 key.WithHelp("c", "copy url"),
875 } else if p.splash.IsClaudeOAuthComplete() {
876 shortList = append(shortList,
878 key.WithKeys("enter"),
879 key.WithHelp("enter", "continue"),
883 shortList = append(shortList,
885 key.WithKeys("enter"),
886 key.WithHelp("enter", "submit"),
890 shortList = append(shortList,
893 key.WithKeys("ctrl+c"),
894 key.WithHelp("ctrl+c", "quit"),
897 // keep them the same
898 for _, v := range shortList {
899 fullList = append(fullList, []key.Binding{v})
901 case p.isOnboarding && !p.splash.IsShowingAPIKey():
902 shortList = append(shortList,
905 key.WithKeys("up", "down"),
906 key.WithHelp("↑/↓", "choose"),
910 key.WithKeys("enter", "ctrl+y"),
911 key.WithHelp("enter", "accept"),
915 key.WithKeys("ctrl+c"),
916 key.WithHelp("ctrl+c", "quit"),
919 // keep them the same
920 for _, v := range shortList {
921 fullList = append(fullList, []key.Binding{v})
923 case p.isOnboarding && p.splash.IsShowingAPIKey():
924 if p.splash.IsAPIKeyValid() {
925 shortList = append(shortList,
927 key.WithKeys("enter"),
928 key.WithHelp("enter", "continue"),
932 shortList = append(shortList,
935 key.WithKeys("esc", "alt+esc"),
936 key.WithHelp("esc", "back"),
940 shortList = append(shortList,
943 key.WithKeys("ctrl+c"),
944 key.WithHelp("ctrl+c", "quit"),
947 // keep them the same
948 for _, v := range shortList {
949 fullList = append(fullList, []key.Binding{v})
951 case p.isProjectInit:
952 shortList = append(shortList,
954 key.WithKeys("ctrl+c"),
955 key.WithHelp("ctrl+c", "quit"),
958 // keep them the same
959 for _, v := range shortList {
960 fullList = append(fullList, []key.Binding{v})
963 if p.editor.IsCompletionsOpen() {
964 shortList = append(shortList,
966 key.WithKeys("tab", "enter"),
967 key.WithHelp("tab/enter", "complete"),
970 key.WithKeys("esc", "alt+esc"),
971 key.WithHelp("esc", "cancel"),
974 key.WithKeys("up", "down"),
975 key.WithHelp("↑/↓", "choose"),
978 for _, v := range shortList {
979 fullList = append(fullList, []key.Binding{v})
981 return core.NewSimpleHelp(shortList, fullList)
983 if p.app.AgentCoordinator != nil && p.app.AgentCoordinator.IsBusy() {
984 cancelBinding := key.NewBinding(
985 key.WithKeys("esc", "alt+esc"),
986 key.WithHelp("esc", "cancel"),
989 cancelBinding = key.NewBinding(
990 key.WithKeys("esc", "alt+esc"),
991 key.WithHelp("esc", "press again to cancel"),
994 if p.app.AgentCoordinator != nil && p.app.AgentCoordinator.QueuedPrompts(p.session.ID) > 0 {
995 cancelBinding = key.NewBinding(
996 key.WithKeys("esc", "alt+esc"),
997 key.WithHelp("esc", "clear queue"),
1000 shortList = append(shortList, cancelBinding)
1001 fullList = append(fullList,
1007 globalBindings := []key.Binding{}
1008 // we are in a session
1009 if p.session.ID != "" {
1010 tabKey := key.NewBinding(
1011 key.WithKeys("tab"),
1012 key.WithHelp("tab", "focus chat"),
1014 if p.focusedPane == PanelTypeChat {
1015 tabKey = key.NewBinding(
1016 key.WithKeys("tab"),
1017 key.WithHelp("tab", "focus editor"),
1020 shortList = append(shortList, tabKey)
1021 globalBindings = append(globalBindings, tabKey)
1023 commandsBinding := key.NewBinding(
1024 key.WithKeys("ctrl+p"),
1025 key.WithHelp("ctrl+p", "commands"),
1027 modelsBinding := key.NewBinding(
1028 key.WithKeys("ctrl+m", "ctrl+l"),
1029 key.WithHelp("ctrl+l", "models"),
1031 if p.keyboardEnhancements.Flags > 0 {
1032 // non-zero flags mean we have at least key disambiguation
1033 modelsBinding.SetHelp("ctrl+m", "models")
1035 helpBinding := key.NewBinding(
1036 key.WithKeys("ctrl+g"),
1037 key.WithHelp("ctrl+g", "more"),
1039 globalBindings = append(globalBindings, commandsBinding, modelsBinding)
1040 globalBindings = append(globalBindings,
1042 key.WithKeys("ctrl+s"),
1043 key.WithHelp("ctrl+s", "sessions"),
1046 if p.session.ID != "" {
1047 globalBindings = append(globalBindings,
1049 key.WithKeys("ctrl+n"),
1050 key.WithHelp("ctrl+n", "new sessions"),
1053 shortList = append(shortList,
1058 fullList = append(fullList, globalBindings)
1060 switch p.focusedPane {
1062 shortList = append(shortList,
1064 key.WithKeys("up", "down"),
1065 key.WithHelp("↑↓", "scroll"),
1069 fullList = append(fullList,
1072 key.WithKeys("up", "down"),
1073 key.WithHelp("↑↓", "scroll"),
1076 key.WithKeys("shift+up", "shift+down"),
1077 key.WithHelp("shift+↑↓", "next/prev item"),
1080 key.WithKeys("pgup", "b"),
1081 key.WithHelp("b/pgup", "page up"),
1084 key.WithKeys("pgdown", " ", "f"),
1085 key.WithHelp("f/pgdn", "page down"),
1091 key.WithHelp("u", "half page up"),
1095 key.WithHelp("d", "half page down"),
1098 key.WithKeys("g", "home"),
1099 key.WithHelp("g", "home"),
1102 key.WithKeys("G", "end"),
1103 key.WithHelp("G", "end"),
1108 messages.ClearSelectionKey,
1111 case PanelTypeEditor:
1112 newLineBinding := key.NewBinding(
1113 key.WithKeys("shift+enter", "ctrl+j"),
1114 // "ctrl+j" is a common keybinding for newline in many editors. If
1115 // the terminal supports "shift+enter", we substitute the help text
1117 key.WithHelp("ctrl+j", "newline"),
1119 if p.keyboardEnhancements.Flags > 0 {
1120 // Non-zero flags mean we have at least key disambiguation.
1121 newLineBinding.SetHelp("shift+enter", newLineBinding.Help().Desc)
1123 shortList = append(shortList, newLineBinding)
1124 fullList = append(fullList,
1128 key.WithKeys("ctrl+f"),
1129 key.WithHelp("ctrl+f", "add image"),
1133 key.WithHelp("@", "mention file"),
1136 key.WithKeys("ctrl+o"),
1137 key.WithHelp("ctrl+o", "open editor"),
1141 if p.editor.HasAttachments() {
1142 fullList = append(fullList, []key.Binding{
1144 key.WithKeys("ctrl+r"),
1145 key.WithHelp("ctrl+r+{i}", "delete attachment at index i"),
1148 key.WithKeys("ctrl+r", "r"),
1149 key.WithHelp("ctrl+r+r", "delete all attachments"),
1152 key.WithKeys("esc", "alt+esc"),
1153 key.WithHelp("esc", "cancel delete mode"),
1158 shortList = append(shortList,
1161 key.WithKeys("ctrl+c"),
1162 key.WithHelp("ctrl+c", "quit"),
1167 fullList = append(fullList, []key.Binding{
1169 key.WithKeys("ctrl+g"),
1170 key.WithHelp("ctrl+g", "less"),
1175 return core.NewSimpleHelp(shortList, fullList)
1178func (p *chatPage) IsChatFocused() bool {
1179 return p.focusedPane == PanelTypeChat
1182// isMouseOverChat checks if the given mouse coordinates are within the chat area bounds.
1183// Returns true if the mouse is over the chat area, false otherwise.
1184func (p *chatPage) isMouseOverChat(x, y int) bool {
1185 // No session means no chat area
1186 if p.session.ID == "" {
1190 var chatX, chatY, chatWidth, chatHeight int
1193 // In compact mode: chat area starts after header and spans full width
1195 chatY = HeaderHeight
1197 chatHeight = p.height - EditorHeight - HeaderHeight
1199 // In non-compact mode: chat area spans from left edge to sidebar
1202 chatWidth = p.width - SideBarWidth
1203 chatHeight = p.height - EditorHeight
1206 // Check if mouse coordinates are within chat bounds
1207 return x >= chatX && x < chatX+chatWidth && y >= chatY && y < chatY+chatHeight
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list