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 var cmds []tea.Cmd
58 switch msg := msg.(type) {
59 case tea.WindowSizeMsg:
60 cmd := p.layout.SetSize(msg.Width, msg.Height)
61 cmds = append(cmds, cmd)
62 case chat.SendMsg:
63 cmd := p.sendMessage(msg.Text)
64 if cmd != nil {
65 return p, cmd
66 }
67 case chat.EditorFocusMsg:
68 p.editingMode = bool(msg)
69 case tea.KeyMsg:
70 switch {
71 case key.Matches(msg, keyMap.NewSession):
72 p.session = session.Session{}
73 return p, tea.Batch(
74 p.clearSidebar(),
75 util.CmdHandler(chat.SessionClearedMsg{}),
76 )
77 case key.Matches(msg, keyMap.Cancel):
78 if p.session.ID != "" {
79 // Cancel the current session's generation process
80 // This allows users to interrupt long-running operations
81 p.app.CoderAgent.Cancel(p.session.ID)
82 return p, nil
83 }
84 }
85 }
86 u, cmd := p.layout.Update(msg)
87 cmds = append(cmds, cmd)
88 p.layout = u.(layout.SplitPaneLayout)
89 return p, tea.Batch(cmds...)
90}
91
92func (p *chatPage) setSidebar() tea.Cmd {
93 sidebarContainer := layout.NewContainer(
94 chat.NewSidebarCmp(p.session, p.app.History),
95 layout.WithPadding(1, 1, 1, 1),
96 )
97 return tea.Batch(p.layout.SetRightPanel(sidebarContainer), sidebarContainer.Init())
98}
99
100func (p *chatPage) clearSidebar() tea.Cmd {
101 return p.layout.SetRightPanel(nil)
102}
103
104func (p *chatPage) sendMessage(text string) tea.Cmd {
105 var cmds []tea.Cmd
106 if p.session.ID == "" {
107 session, err := p.app.Sessions.Create(context.Background(), "New Session")
108 if err != nil {
109 return util.ReportError(err)
110 }
111
112 p.session = session
113 cmd := p.setSidebar()
114 if cmd != nil {
115 cmds = append(cmds, cmd)
116 }
117 cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
118 }
119
120 p.app.CoderAgent.Run(context.Background(), p.session.ID, text)
121 return tea.Batch(cmds...)
122}
123
124func (p *chatPage) SetSize(width, height int) tea.Cmd {
125 return p.layout.SetSize(width, height)
126}
127
128func (p *chatPage) GetSize() (int, int) {
129 return p.layout.GetSize()
130}
131
132func (p *chatPage) View() string {
133 return p.layout.View()
134}
135
136func (p *chatPage) BindingKeys() []key.Binding {
137 bindings := layout.KeyMapToSlice(keyMap)
138 if p.editingMode {
139 bindings = append(bindings, p.editor.BindingKeys()...)
140 } else {
141 bindings = append(bindings, p.messages.BindingKeys()...)
142 }
143 return bindings
144}
145
146func NewChatPage(app *app.App) tea.Model {
147 messagesContainer := layout.NewContainer(
148 chat.NewMessagesCmp(app),
149 layout.WithPadding(1, 1, 0, 1),
150 )
151
152 editorContainer := layout.NewContainer(
153 chat.NewEditorCmp(app),
154 layout.WithBorder(true, false, false, false),
155 )
156 return &chatPage{
157 app: app,
158 editor: editorContainer,
159 messages: messagesContainer,
160 editingMode: true,
161 layout: layout.NewSplitPane(
162 layout.WithLeftPanel(messagesContainer),
163 layout.WithBottomPanel(editorContainer),
164 ),
165 }
166}