1package chat
2
3import (
4 "context"
5
6 "github.com/charmbracelet/bubbles/v2/key"
7 tea "github.com/charmbracelet/bubbletea/v2"
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/chat/editor"
13 "github.com/opencode-ai/opencode/internal/tui/components/chat/sidebar"
14 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/commands"
15 "github.com/opencode-ai/opencode/internal/tui/layout"
16 "github.com/opencode-ai/opencode/internal/tui/page"
17 "github.com/opencode-ai/opencode/internal/tui/util"
18)
19
20var ChatPage page.PageID = "chat"
21
22type chatPage struct {
23 app *app.App
24
25 layout layout.SplitPaneLayout
26
27 session session.Session
28}
29
30type ChatKeyMap struct {
31 NewSession key.Binding
32 Cancel key.Binding
33}
34
35var keyMap = ChatKeyMap{
36 NewSession: key.NewBinding(
37 key.WithKeys("ctrl+n"),
38 key.WithHelp("ctrl+n", "new session"),
39 ),
40 Cancel: key.NewBinding(
41 key.WithKeys("esc"),
42 key.WithHelp("esc", "cancel"),
43 ),
44}
45
46func (p *chatPage) Init() tea.Cmd {
47 return p.layout.Init()
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 commands.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
67 // Handle custom command execution
68 cmd := p.sendMessage(msg.Content, nil)
69 if cmd != nil {
70 return p, cmd
71 }
72 case chat.SessionSelectedMsg:
73 if p.session.ID == "" {
74 cmd := p.setMessages()
75 if cmd != nil {
76 cmds = append(cmds, cmd)
77 }
78 }
79 p.session = msg
80 case tea.KeyPressMsg:
81 switch {
82 case key.Matches(msg, keyMap.NewSession):
83 p.session = session.Session{}
84 return p, tea.Batch(
85 p.clearMessages(),
86 util.CmdHandler(chat.SessionClearedMsg{}),
87 )
88 case key.Matches(msg, keyMap.Cancel):
89 if p.session.ID != "" {
90 // Cancel the current session's generation process
91 // This allows users to interrupt long-running operations
92 p.app.CoderAgent.Cancel(p.session.ID)
93 return p, nil
94 }
95 }
96 }
97 u, cmd := p.layout.Update(msg)
98 cmds = append(cmds, cmd)
99 p.layout = u.(layout.SplitPaneLayout)
100
101 return p, tea.Batch(cmds...)
102}
103
104func (p *chatPage) setMessages() tea.Cmd {
105 messagesContainer := layout.NewContainer(
106 chat.NewMessagesListCmp(p.app),
107 layout.WithPadding(1, 1, 0, 1),
108 )
109 return tea.Batch(p.layout.SetLeftPanel(messagesContainer), messagesContainer.Init())
110}
111
112func (p *chatPage) clearMessages() tea.Cmd {
113 return p.layout.ClearLeftPanel()
114}
115
116func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
117 var cmds []tea.Cmd
118 if p.session.ID == "" {
119 session, err := p.app.Sessions.Create(context.Background(), "New Session")
120 if err != nil {
121 return util.ReportError(err)
122 }
123
124 p.session = session
125 cmd := p.setMessages()
126 if cmd != nil {
127 cmds = append(cmds, cmd)
128 }
129 cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
130 }
131
132 _, err := p.app.CoderAgent.Run(context.Background(), p.session.ID, text, attachments...)
133 if err != nil {
134 return util.ReportError(err)
135 }
136 return tea.Batch(cmds...)
137}
138
139func (p *chatPage) SetSize(width, height int) tea.Cmd {
140 return p.layout.SetSize(width, height)
141}
142
143func (p *chatPage) GetSize() (int, int) {
144 return p.layout.GetSize()
145}
146
147func (p *chatPage) View() tea.View {
148 return p.layout.View()
149}
150
151func (p *chatPage) BindingKeys() []key.Binding {
152 bindings := layout.KeyMapToSlice(keyMap)
153 return bindings
154}
155
156func NewChatPage(app *app.App) util.Model {
157 sidebarContainer := layout.NewContainer(
158 sidebar.NewSidebarCmp(),
159 layout.WithPadding(1, 1, 1, 1),
160 )
161 editorContainer := layout.NewContainer(
162 editor.NewEditorCmp(app),
163 )
164 return &chatPage{
165 app: app,
166 layout: layout.NewSplitPane(
167 layout.WithRightPanel(sidebarContainer),
168 layout.WithBottomPanel(editorContainer),
169 layout.WithFixedBottomHeight(3),
170 layout.WithFixedRightWidth(31),
171 ),
172 }
173}