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/logging"
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/chat/editor"
14 "github.com/opencode-ai/opencode/internal/tui/components/chat/sidebar"
15 "github.com/opencode-ai/opencode/internal/tui/components/dialogs/commands"
16 "github.com/opencode-ai/opencode/internal/tui/layout"
17 "github.com/opencode-ai/opencode/internal/tui/page"
18 "github.com/opencode-ai/opencode/internal/tui/util"
19)
20
21var ChatPage page.PageID = "chat"
22
23type ChatFocusedMsg struct {
24 Focused bool // True if the chat input is focused, false otherwise
25}
26
27type chatPage struct {
28 app *app.App
29
30 layout layout.SplitPaneLayout
31
32 session session.Session
33
34 keyMap KeyMap
35
36 chatFocused bool
37}
38
39func (p *chatPage) Init() tea.Cmd {
40 cmd := p.layout.Init()
41 return tea.Batch(
42 cmd,
43 p.layout.FocusPanel(layout.BottomPanel), // Focus on the bottom panel (editor)
44 )
45}
46
47func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
48 var cmds []tea.Cmd
49 switch msg := msg.(type) {
50 case tea.WindowSizeMsg:
51 cmd := p.layout.SetSize(msg.Width, msg.Height)
52 cmds = append(cmds, cmd)
53 case chat.SendMsg:
54 cmd := p.sendMessage(msg.Text, msg.Attachments)
55 if cmd != nil {
56 return p, cmd
57 }
58 case commands.CommandRunCustomMsg:
59 // Check if the agent is busy before executing custom commands
60 if p.app.CoderAgent.IsBusy() {
61 return p, util.ReportWarn("Agent is busy, please wait before executing a command...")
62 }
63
64 // Handle custom command execution
65 cmd := p.sendMessage(msg.Content, nil)
66 if cmd != nil {
67 return p, cmd
68 }
69 case chat.SessionSelectedMsg:
70 if p.session.ID == "" {
71 cmd := p.setMessages()
72 if cmd != nil {
73 cmds = append(cmds, cmd)
74 }
75 }
76 p.session = msg
77 case tea.KeyPressMsg:
78 switch {
79 case key.Matches(msg, p.keyMap.NewSession):
80 p.session = session.Session{}
81 return p, tea.Batch(
82 p.clearMessages(),
83 util.CmdHandler(chat.SessionClearedMsg{}),
84 )
85
86 case key.Matches(msg, p.keyMap.Tab):
87 logging.Info("Tab key pressed, toggling chat focus")
88 if p.session.ID == "" {
89 return p, nil
90 }
91 p.chatFocused = !p.chatFocused
92 if p.chatFocused {
93 cmds = append(cmds, p.layout.FocusPanel(layout.LeftPanel))
94 cmds = append(cmds, util.CmdHandler(ChatFocusedMsg{Focused: true}))
95 } else {
96 cmds = append(cmds, p.layout.FocusPanel(layout.BottomPanel))
97 cmds = append(cmds, util.CmdHandler(ChatFocusedMsg{Focused: false}))
98 }
99 return p, tea.Batch(cmds...)
100 case key.Matches(msg, p.keyMap.Cancel):
101 if p.session.ID != "" {
102 // Cancel the current session's generation process
103 // This allows users to interrupt long-running operations
104 p.app.CoderAgent.Cancel(p.session.ID)
105 return p, nil
106 }
107 }
108 }
109 u, cmd := p.layout.Update(msg)
110 cmds = append(cmds, cmd)
111 p.layout = u.(layout.SplitPaneLayout)
112
113 return p, tea.Batch(cmds...)
114}
115
116func (p *chatPage) setMessages() tea.Cmd {
117 messagesContainer := layout.NewContainer(
118 chat.NewMessagesListCmp(p.app),
119 layout.WithPadding(1, 1, 0, 1),
120 )
121 return tea.Batch(p.layout.SetLeftPanel(messagesContainer), messagesContainer.Init())
122}
123
124func (p *chatPage) clearMessages() tea.Cmd {
125 return p.layout.ClearLeftPanel()
126}
127
128func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
129 var cmds []tea.Cmd
130 if p.session.ID == "" {
131 session, err := p.app.Sessions.Create(context.Background(), "New Session")
132 if err != nil {
133 return util.ReportError(err)
134 }
135
136 p.session = session
137 cmd := p.setMessages()
138 if cmd != nil {
139 cmds = append(cmds, cmd)
140 }
141 cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
142 }
143
144 _, err := p.app.CoderAgent.Run(context.Background(), p.session.ID, text, attachments...)
145 if err != nil {
146 return util.ReportError(err)
147 }
148 return tea.Batch(cmds...)
149}
150
151func (p *chatPage) SetSize(width, height int) tea.Cmd {
152 return p.layout.SetSize(width, height)
153}
154
155func (p *chatPage) GetSize() (int, int) {
156 return p.layout.GetSize()
157}
158
159func (p *chatPage) View() tea.View {
160 return p.layout.View()
161}
162
163func NewChatPage(app *app.App) util.Model {
164 sidebarContainer := layout.NewContainer(
165 sidebar.NewSidebarCmp(),
166 layout.WithPadding(1, 1, 1, 1),
167 )
168 editorContainer := layout.NewContainer(
169 editor.NewEditorCmp(app),
170 )
171 return &chatPage{
172 app: app,
173 layout: layout.NewSplitPane(
174 layout.WithRightPanel(sidebarContainer),
175 layout.WithBottomPanel(editorContainer),
176 layout.WithFixedBottomHeight(5),
177 layout.WithFixedRightWidth(31),
178 ),
179 keyMap: DefaultKeyMap(),
180 }
181}