chat.go

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