1package page
2
3import (
4 "context"
5
6 "github.com/charmbracelet/bubbles/key"
7 tea "github.com/charmbracelet/bubbletea"
8 "github.com/kujtimiihoxha/opencode/internal/app"
9 "github.com/kujtimiihoxha/opencode/internal/session"
10 "github.com/kujtimiihoxha/opencode/internal/tui/components/chat"
11 "github.com/kujtimiihoxha/opencode/internal/tui/layout"
12 "github.com/kujtimiihoxha/opencode/internal/tui/util"
13)
14
15var ChatPage PageID = "chat"
16
17type chatPage struct {
18 app *app.App
19 editor layout.Container
20 messages layout.Container
21 layout layout.SplitPaneLayout
22 session session.Session
23 editingMode bool
24}
25
26type ChatKeyMap struct {
27 NewSession key.Binding
28 Cancel key.Binding
29}
30
31var keyMap = ChatKeyMap{
32 NewSession: key.NewBinding(
33 key.WithKeys("ctrl+n"),
34 key.WithHelp("ctrl+n", "new session"),
35 ),
36 Cancel: key.NewBinding(
37 key.WithKeys("ctrl+x"),
38 key.WithHelp("ctrl+x", "cancel"),
39 ),
40}
41
42func (p *chatPage) Init() tea.Cmd {
43 cmds := []tea.Cmd{
44 p.layout.Init(),
45 }
46
47 sessions, _ := p.app.Sessions.List(context.Background())
48 if len(sessions) > 0 {
49 p.session = sessions[0]
50 cmd := p.setSidebar()
51 cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(p.session)), cmd)
52 }
53 return tea.Batch(cmds...)
54}
55
56func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
57 switch msg := msg.(type) {
58 case tea.WindowSizeMsg:
59 p.layout.SetSize(msg.Width, msg.Height)
60 case chat.SendMsg:
61 cmd := p.sendMessage(msg.Text)
62 if cmd != nil {
63 return p, cmd
64 }
65 case chat.EditorFocusMsg:
66 p.editingMode = bool(msg)
67 case tea.KeyMsg:
68 switch {
69 case key.Matches(msg, keyMap.NewSession):
70 p.session = session.Session{}
71 p.clearSidebar()
72 return p, util.CmdHandler(chat.SessionClearedMsg{})
73 case key.Matches(msg, keyMap.Cancel):
74 if p.session.ID != "" {
75 // Cancel the current session's generation process
76 // This allows users to interrupt long-running operations
77 p.app.CoderAgent.Cancel(p.session.ID)
78 return p, nil
79 }
80 }
81 }
82 u, cmd := p.layout.Update(msg)
83 p.layout = u.(layout.SplitPaneLayout)
84 if cmd != nil {
85 return p, cmd
86 }
87 return p, nil
88}
89
90func (p *chatPage) setSidebar() tea.Cmd {
91 sidebarContainer := layout.NewContainer(
92 chat.NewSidebarCmp(p.session, p.app.History),
93 layout.WithPadding(1, 1, 1, 1),
94 )
95 p.layout.SetRightPanel(sidebarContainer)
96 width, height := p.layout.GetSize()
97 p.layout.SetSize(width, height)
98 return sidebarContainer.Init()
99}
100
101func (p *chatPage) clearSidebar() {
102 p.layout.SetRightPanel(nil)
103 width, height := p.layout.GetSize()
104 p.layout.SetSize(width, height)
105}
106
107func (p *chatPage) sendMessage(text string) tea.Cmd {
108 var cmds []tea.Cmd
109 if p.session.ID == "" {
110 session, err := p.app.Sessions.Create(context.Background(), "New Session")
111 if err != nil {
112 return util.ReportError(err)
113 }
114
115 p.session = session
116 cmd := p.setSidebar()
117 if cmd != nil {
118 cmds = append(cmds, cmd)
119 }
120 cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
121 }
122
123 p.app.CoderAgent.Run(context.Background(), p.session.ID, text)
124 return tea.Batch(cmds...)
125}
126
127func (p *chatPage) SetSize(width, height int) {
128 p.layout.SetSize(width, height)
129}
130
131func (p *chatPage) GetSize() (int, int) {
132 return p.layout.GetSize()
133}
134
135func (p *chatPage) View() string {
136 return p.layout.View()
137}
138
139func (p *chatPage) BindingKeys() []key.Binding {
140 bindings := layout.KeyMapToSlice(keyMap)
141 if p.editingMode {
142 bindings = append(bindings, p.editor.BindingKeys()...)
143 } else {
144 bindings = append(bindings, p.messages.BindingKeys()...)
145 }
146 return bindings
147}
148
149func NewChatPage(app *app.App) tea.Model {
150 messagesContainer := layout.NewContainer(
151 chat.NewMessagesCmp(app),
152 layout.WithPadding(1, 1, 0, 1),
153 )
154
155 editorContainer := layout.NewContainer(
156 chat.NewEditorCmp(app),
157 layout.WithBorder(true, false, false, false),
158 )
159 return &chatPage{
160 app: app,
161 editor: editorContainer,
162 messages: messagesContainer,
163 editingMode: true,
164 layout: layout.NewSplitPane(
165 layout.WithLeftPanel(messagesContainer),
166 layout.WithBottomPanel(editorContainer),
167 ),
168 }
169}