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}