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}