chat.go

  1package chat
  2
  3import (
  4	"context"
  5
  6	"github.com/charmbracelet/bubbles/v2/key"
  7	tea "github.com/charmbracelet/bubbletea/v2"
  8	"github.com/opencode-ai/opencode/internal/app"
  9	"github.com/opencode-ai/opencode/internal/logging"
 10	"github.com/opencode-ai/opencode/internal/message"
 11	"github.com/opencode-ai/opencode/internal/session"
 12	"github.com/opencode-ai/opencode/internal/tui/components/chat"
 13	"github.com/opencode-ai/opencode/internal/tui/components/chat/editor"
 14	"github.com/opencode-ai/opencode/internal/tui/components/chat/sidebar"
 15	"github.com/opencode-ai/opencode/internal/tui/components/dialogs/commands"
 16	"github.com/opencode-ai/opencode/internal/tui/layout"
 17	"github.com/opencode-ai/opencode/internal/tui/page"
 18	"github.com/opencode-ai/opencode/internal/tui/util"
 19)
 20
 21var ChatPage page.PageID = "chat"
 22
 23type ChatFocusedMsg struct {
 24	Focused bool // True if the chat input is focused, false otherwise
 25}
 26
 27type chatPage struct {
 28	app *app.App
 29
 30	layout layout.SplitPaneLayout
 31
 32	session session.Session
 33
 34	keyMap KeyMap
 35
 36	chatFocused bool
 37}
 38
 39func (p *chatPage) Init() tea.Cmd {
 40	cmd := p.layout.Init()
 41	return tea.Batch(
 42		cmd,
 43		p.layout.FocusPanel(layout.BottomPanel), // Focus on the bottom panel (editor)
 44	)
 45}
 46
 47func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 48	var cmds []tea.Cmd
 49	switch msg := msg.(type) {
 50	case tea.WindowSizeMsg:
 51		cmd := p.layout.SetSize(msg.Width, msg.Height)
 52		cmds = append(cmds, cmd)
 53	case chat.SendMsg:
 54		cmd := p.sendMessage(msg.Text, msg.Attachments)
 55		if cmd != nil {
 56			return p, cmd
 57		}
 58	case commands.CommandRunCustomMsg:
 59		// Check if the agent is busy before executing custom commands
 60		if p.app.CoderAgent.IsBusy() {
 61			return p, util.ReportWarn("Agent is busy, please wait before executing a command...")
 62		}
 63
 64		// Handle custom command execution
 65		cmd := p.sendMessage(msg.Content, nil)
 66		if cmd != nil {
 67			return p, cmd
 68		}
 69	case chat.SessionSelectedMsg:
 70		if p.session.ID == "" {
 71			cmd := p.setMessages()
 72			if cmd != nil {
 73				cmds = append(cmds, cmd)
 74			}
 75		}
 76		p.session = msg
 77	case tea.KeyPressMsg:
 78		switch {
 79		case key.Matches(msg, p.keyMap.NewSession):
 80			p.session = session.Session{}
 81			return p, tea.Batch(
 82				p.clearMessages(),
 83				util.CmdHandler(chat.SessionClearedMsg{}),
 84			)
 85
 86		case key.Matches(msg, p.keyMap.Tab):
 87			logging.Info("Tab key pressed, toggling chat focus")
 88			if p.session.ID == "" {
 89				return p, nil
 90			}
 91			p.chatFocused = !p.chatFocused
 92			if p.chatFocused {
 93				cmds = append(cmds, p.layout.FocusPanel(layout.LeftPanel))
 94				cmds = append(cmds, util.CmdHandler(ChatFocusedMsg{Focused: true}))
 95			} else {
 96				cmds = append(cmds, p.layout.FocusPanel(layout.BottomPanel))
 97				cmds = append(cmds, util.CmdHandler(ChatFocusedMsg{Focused: false}))
 98			}
 99			return p, tea.Batch(cmds...)
100		case key.Matches(msg, p.keyMap.Cancel):
101			if p.session.ID != "" {
102				// Cancel the current session's generation process
103				// This allows users to interrupt long-running operations
104				p.app.CoderAgent.Cancel(p.session.ID)
105				return p, nil
106			}
107		}
108	}
109	u, cmd := p.layout.Update(msg)
110	cmds = append(cmds, cmd)
111	p.layout = u.(layout.SplitPaneLayout)
112
113	return p, tea.Batch(cmds...)
114}
115
116func (p *chatPage) setMessages() tea.Cmd {
117	messagesContainer := layout.NewContainer(
118		chat.NewMessagesListCmp(p.app),
119		layout.WithPadding(1, 1, 0, 1),
120	)
121	return tea.Batch(p.layout.SetLeftPanel(messagesContainer), messagesContainer.Init())
122}
123
124func (p *chatPage) clearMessages() tea.Cmd {
125	return p.layout.ClearLeftPanel()
126}
127
128func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
129	var cmds []tea.Cmd
130	if p.session.ID == "" {
131		session, err := p.app.Sessions.Create(context.Background(), "New Session")
132		if err != nil {
133			return util.ReportError(err)
134		}
135
136		p.session = session
137		cmd := p.setMessages()
138		if cmd != nil {
139			cmds = append(cmds, cmd)
140		}
141		cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
142	}
143
144	_, err := p.app.CoderAgent.Run(context.Background(), p.session.ID, text, attachments...)
145	if err != nil {
146		return util.ReportError(err)
147	}
148	return tea.Batch(cmds...)
149}
150
151func (p *chatPage) SetSize(width, height int) tea.Cmd {
152	return p.layout.SetSize(width, height)
153}
154
155func (p *chatPage) GetSize() (int, int) {
156	return p.layout.GetSize()
157}
158
159func (p *chatPage) View() tea.View {
160	return p.layout.View()
161}
162
163func NewChatPage(app *app.App) util.Model {
164	sidebarContainer := layout.NewContainer(
165		sidebar.NewSidebarCmp(),
166		layout.WithPadding(1, 1, 1, 1),
167	)
168	editorContainer := layout.NewContainer(
169		editor.NewEditorCmp(app),
170	)
171	return &chatPage{
172		app: app,
173		layout: layout.NewSplitPane(
174			layout.WithRightPanel(sidebarContainer),
175			layout.WithBottomPanel(editorContainer),
176			layout.WithFixedBottomHeight(5),
177			layout.WithFixedRightWidth(31),
178		),
179		keyMap: DefaultKeyMap(),
180	}
181}