@@ -0,0 +1,173 @@
1+package chat
2+
3+import (
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/message"
10+ "github.com/opencode-ai/opencode/internal/session"
11+ "github.com/opencode-ai/opencode/internal/tui/components/chat"
12+ "github.com/opencode-ai/opencode/internal/tui/components/chat/editor"
13+ "github.com/opencode-ai/opencode/internal/tui/components/chat/sidebar"
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/page"
17+ "github.com/opencode-ai/opencode/internal/tui/util"
18+)
19+
20+var ChatPage page.PageID = "chat"
21+
22+type chatPage struct {
23+ app *app.App
24+
25+ layout layout.SplitPaneLayout
26+
27+ session session.Session
28+}
29+
30+type ChatKeyMap struct {
31+ NewSession key.Binding
32+ Cancel key.Binding
33+}
34+
35+var keyMap = ChatKeyMap{
36+ NewSession: key.NewBinding(
37+ key.WithKeys("ctrl+n"),
38+ key.WithHelp("ctrl+n", "new session"),
39+ ),
40+ Cancel: key.NewBinding(
41+ key.WithKeys("esc"),
42+ key.WithHelp("esc", "cancel"),
43+ ),
44+}
45+
46+func (p *chatPage) Init() tea.Cmd {
47+ return p.layout.Init()
48+}
49+
50+func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
51+ var cmds []tea.Cmd
52+ switch msg := msg.(type) {
53+ case tea.WindowSizeMsg:
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.setMessages()
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.clearMessages(),
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+
104+func (p *chatPage) setMessages() tea.Cmd {
105+ messagesContainer := layout.NewContainer(
106+ chat.NewMessagesListCmp(p.app),
107+ layout.WithPadding(1, 1, 0, 1),
108+ )
109+ return tea.Batch(p.layout.SetLeftPanel(messagesContainer), messagesContainer.Init())
110+}
111+
112+func (p *chatPage) clearMessages() tea.Cmd {
113+ return p.layout.ClearLeftPanel()
114+}
115+
116+func (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.setMessages()
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+
139+func (p *chatPage) SetSize(width, height int) tea.Cmd {
140+ return p.layout.SetSize(width, height)
141+}
142+
143+func (p *chatPage) GetSize() (int, int) {
144+ return p.layout.GetSize()
145+}
146+
147+func (p *chatPage) View() tea.View {
148+ return p.layout.View()
149+}
150+
151+func (p *chatPage) BindingKeys() []key.Binding {
152+ bindings := layout.KeyMapToSlice(keyMap)
153+ return bindings
154+}
155+
156+func NewChatPage(app *app.App) util.Model {
157+ sidebarContainer := layout.NewContainer(
158+ sidebar.NewSidebarCmp(),
159+ layout.WithPadding(1, 1, 1, 1),
160+ )
161+ editorContainer := layout.NewContainer(
162+ editor.NewEditorCmp(app),
163+ )
164+ return &chatPage{
165+ app: app,
166+ layout: layout.NewSplitPane(
167+ layout.WithRightPanel(sidebarContainer),
168+ layout.WithBottomPanel(editorContainer),
169+ layout.WithFixedBottomHeight(3),
170+ layout.WithFixedRightWidth(31),
171+ ),
172+ }
173+}