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}