chat.go

  1package page
  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/dialogs/commands"
 15	"github.com/opencode-ai/opencode/internal/tui/layout"
 16	"github.com/opencode-ai/opencode/internal/tui/util"
 17)
 18
 19var ChatPage PageID = "chat"
 20
 21type chatPage struct {
 22	app      *app.App
 23	editor   layout.Container
 24	messages layout.Container
 25	layout   layout.SplitPaneLayout
 26	session  session.Session
 27}
 28
 29type ChatKeyMap struct {
 30	NewSession key.Binding
 31	Cancel     key.Binding
 32}
 33
 34var keyMap = ChatKeyMap{
 35	NewSession: key.NewBinding(
 36		key.WithKeys("ctrl+n"),
 37		key.WithHelp("ctrl+n", "new session"),
 38	),
 39	Cancel: key.NewBinding(
 40		key.WithKeys("esc"),
 41		key.WithHelp("esc", "cancel"),
 42	),
 43}
 44
 45func (p *chatPage) Init() tea.Cmd {
 46	return p.layout.Init()
 47}
 48
 49func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 50	var cmds []tea.Cmd
 51	switch msg := msg.(type) {
 52	case tea.WindowSizeMsg:
 53		logging.Info("Window size changed Chat:", "Width", msg.Width, "Height", msg.Height)
 54		cmd := p.layout.SetSize(msg.Width, msg.Height)
 55		cmds = append(cmds, cmd)
 56	case chat.SendMsg:
 57		cmd := p.sendMessage(msg.Text, msg.Attachments)
 58		if cmd != nil {
 59			return p, cmd
 60		}
 61	case commands.CommandRunCustomMsg:
 62		// Check if the agent is busy before executing custom commands
 63		if p.app.CoderAgent.IsBusy() {
 64			return p, util.ReportWarn("Agent is busy, please wait before executing a command...")
 65		}
 66
 67		// Handle custom command execution
 68		cmd := p.sendMessage(msg.Content, nil)
 69		if cmd != nil {
 70			return p, cmd
 71		}
 72	case chat.SessionSelectedMsg:
 73		if p.session.ID == "" {
 74			cmd := p.setSidebar()
 75			if cmd != nil {
 76				cmds = append(cmds, cmd)
 77			}
 78		}
 79		p.session = msg
 80	case tea.KeyPressMsg:
 81		switch {
 82		case key.Matches(msg, keyMap.NewSession):
 83			p.session = session.Session{}
 84			return p, tea.Batch(
 85				p.clearSidebar(),
 86				util.CmdHandler(chat.SessionClearedMsg{}),
 87			)
 88		case key.Matches(msg, keyMap.Cancel):
 89			if p.session.ID != "" {
 90				// Cancel the current session's generation process
 91				// This allows users to interrupt long-running operations
 92				p.app.CoderAgent.Cancel(p.session.ID)
 93				return p, nil
 94			}
 95		}
 96	}
 97	u, cmd := p.layout.Update(msg)
 98	cmds = append(cmds, cmd)
 99	p.layout = u.(layout.SplitPaneLayout)
100
101	return p, tea.Batch(cmds...)
102}
103
104func (p *chatPage) setSidebar() tea.Cmd {
105	sidebarContainer := layout.NewContainer(
106		chat.NewSidebarCmp(p.session, p.app.History),
107		layout.WithPadding(1, 1, 1, 1),
108	)
109	return tea.Batch(p.layout.SetRightPanel(sidebarContainer), sidebarContainer.Init())
110}
111
112func (p *chatPage) clearSidebar() tea.Cmd {
113	return p.layout.ClearRightPanel()
114}
115
116func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
117	var cmds []tea.Cmd
118	if p.session.ID == "" {
119		session, err := p.app.Sessions.Create(context.Background(), "New Session")
120		if err != nil {
121			return util.ReportError(err)
122		}
123
124		p.session = session
125		cmd := p.setSidebar()
126		if cmd != nil {
127			cmds = append(cmds, cmd)
128		}
129		cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
130	}
131
132	_, err := p.app.CoderAgent.Run(context.Background(), p.session.ID, text, attachments...)
133	if err != nil {
134		return util.ReportError(err)
135	}
136	return tea.Batch(cmds...)
137}
138
139func (p *chatPage) SetSize(width, height int) tea.Cmd {
140	return p.layout.SetSize(width, height)
141}
142
143func (p *chatPage) GetSize() (int, int) {
144	return p.layout.GetSize()
145}
146
147func (p *chatPage) View() tea.View {
148	return p.layout.View()
149}
150
151func (p *chatPage) BindingKeys() []key.Binding {
152	bindings := layout.KeyMapToSlice(keyMap)
153	bindings = append(bindings, p.messages.BindingKeys()...)
154	bindings = append(bindings, p.editor.BindingKeys()...)
155	return bindings
156}
157
158func NewChatPage(app *app.App) util.Model {
159	messagesContainer := layout.NewContainer(
160		chat.NewMessagesListCmp(app),
161		layout.WithPadding(1, 1, 0, 1),
162	)
163	editorContainer := layout.NewContainer(
164		editor.NewEditorCmp(app),
165		layout.WithBorder(true, false, false, false),
166	)
167	return &chatPage{
168		app:      app,
169		editor:   editorContainer,
170		messages: messagesContainer,
171		layout: layout.NewSplitPane(
172			layout.WithLeftPanel(messagesContainer),
173			layout.WithBottomPanel(editorContainer),
174		),
175	}
176}