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}
24
25type ChatKeyMap struct {
26 NewSession key.Binding
27 Cancel key.Binding
28}
29
30var keyMap = ChatKeyMap{
31 NewSession: key.NewBinding(
32 key.WithKeys("ctrl+n"),
33 key.WithHelp("ctrl+n", "new session"),
34 ),
35 Cancel: key.NewBinding(
36 key.WithKeys("esc"),
37 key.WithHelp("esc", "cancel"),
38 ),
39}
40
41func (p *chatPage) Init() tea.Cmd {
42 cmds := []tea.Cmd{
43 p.layout.Init(),
44 }
45 return tea.Batch(cmds...)
46}
47
48func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
49 var cmds []tea.Cmd
50 switch msg := msg.(type) {
51 case tea.WindowSizeMsg:
52 cmd := p.layout.SetSize(msg.Width, msg.Height)
53 cmds = append(cmds, cmd)
54 case chat.SendMsg:
55 cmd := p.sendMessage(msg.Text)
56 if cmd != nil {
57 return p, cmd
58 }
59 case chat.SessionSelectedMsg:
60 if p.session.ID == "" {
61 cmd := p.setSidebar()
62 if cmd != nil {
63 cmds = append(cmds, cmd)
64 }
65 }
66 p.session = msg
67 case tea.KeyMsg:
68 switch {
69 case key.Matches(msg, keyMap.NewSession):
70 p.session = session.Session{}
71 return p, tea.Batch(
72 p.clearSidebar(),
73 util.CmdHandler(chat.SessionClearedMsg{}),
74 )
75 case key.Matches(msg, keyMap.Cancel):
76 if p.session.ID != "" {
77 // Cancel the current session's generation process
78 // This allows users to interrupt long-running operations
79 p.app.CoderAgent.Cancel(p.session.ID)
80 return p, nil
81 }
82 }
83 }
84 u, cmd := p.layout.Update(msg)
85 cmds = append(cmds, cmd)
86 p.layout = u.(layout.SplitPaneLayout)
87 return p, tea.Batch(cmds...)
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 return tea.Batch(p.layout.SetRightPanel(sidebarContainer), sidebarContainer.Init())
96}
97
98func (p *chatPage) clearSidebar() tea.Cmd {
99 return p.layout.ClearRightPanel()
100}
101
102func (p *chatPage) sendMessage(text string) tea.Cmd {
103 var cmds []tea.Cmd
104 if p.session.ID == "" {
105 session, err := p.app.Sessions.Create(context.Background(), "New Session")
106 if err != nil {
107 return util.ReportError(err)
108 }
109
110 p.session = session
111 cmd := p.setSidebar()
112 if cmd != nil {
113 cmds = append(cmds, cmd)
114 }
115 cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
116 }
117
118 p.app.CoderAgent.Run(context.Background(), p.session.ID, text)
119 return tea.Batch(cmds...)
120}
121
122func (p *chatPage) SetSize(width, height int) tea.Cmd {
123 return p.layout.SetSize(width, height)
124}
125
126func (p *chatPage) GetSize() (int, int) {
127 return p.layout.GetSize()
128}
129
130func (p *chatPage) View() string {
131 return p.layout.View()
132}
133
134func (p *chatPage) BindingKeys() []key.Binding {
135 bindings := layout.KeyMapToSlice(keyMap)
136 bindings = append(bindings, p.messages.BindingKeys()...)
137 return bindings
138}
139
140func NewChatPage(app *app.App) tea.Model {
141 messagesContainer := layout.NewContainer(
142 chat.NewMessagesCmp(app),
143 layout.WithPadding(1, 1, 0, 1),
144 )
145
146 editorContainer := layout.NewContainer(
147 chat.NewEditorCmp(app),
148 layout.WithBorder(true, false, false, false),
149 )
150 return &chatPage{
151 app: app,
152 editor: editorContainer,
153 messages: messagesContainer,
154 layout: layout.NewSplitPane(
155 layout.WithLeftPanel(messagesContainer),
156 layout.WithBottomPanel(editorContainer),
157 ),
158 }
159}