1package page
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/dialogs/commands"
15 "github.com/opencode-ai/opencode/internal/tui/layout"
16 "github.com/opencode-ai/opencode/internal/tui/util"
17)
18
19var ChatPage PageID = "chat"
20
21type chatPage struct {
22 app *app.App
23 editor layout.Container
24 messages layout.Container
25 layout layout.SplitPaneLayout
26 session session.Session
27}
28
29type ChatKeyMap struct {
30 NewSession key.Binding
31 Cancel key.Binding
32}
33
34var keyMap = ChatKeyMap{
35 NewSession: key.NewBinding(
36 key.WithKeys("ctrl+n"),
37 key.WithHelp("ctrl+n", "new session"),
38 ),
39 Cancel: key.NewBinding(
40 key.WithKeys("esc"),
41 key.WithHelp("esc", "cancel"),
42 ),
43}
44
45func (p *chatPage) Init() tea.Cmd {
46 return p.layout.Init()
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 logging.Info("Window size changed Chat:", "Width", msg.Width, "Height", msg.Height)
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.setSidebar()
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.clearSidebar(),
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) setSidebar() tea.Cmd {
105 sidebarContainer := layout.NewContainer(
106 chat.NewSidebarCmp(p.session, p.app.History),
107 layout.WithPadding(1, 1, 1, 1),
108 )
109 return tea.Batch(p.layout.SetRightPanel(sidebarContainer), sidebarContainer.Init())
110}
111
112func (p *chatPage) clearSidebar() tea.Cmd {
113 return p.layout.ClearRightPanel()
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.setSidebar()
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 bindings = append(bindings, p.messages.BindingKeys()...)
154 bindings = append(bindings, p.editor.BindingKeys()...)
155 return bindings
156}
157
158func NewChatPage(app *app.App) util.Model {
159 messagesContainer := layout.NewContainer(
160 chat.NewMessagesListCmp(app),
161 layout.WithPadding(1, 1, 0, 1),
162 )
163 editorContainer := layout.NewContainer(
164 editor.NewEditorCmp(app),
165 layout.WithBorder(true, false, false, false),
166 )
167 return &chatPage{
168 app: app,
169 editor: editorContainer,
170 messages: messagesContainer,
171 layout: layout.NewSplitPane(
172 layout.WithLeftPanel(messagesContainer),
173 layout.WithBottomPanel(editorContainer),
174 ),
175 }
176}