chat.go

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