chat.go

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