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