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