chat.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. package page
  2. import (
  3. "context"
  4. "strings"
  5. "github.com/charmbracelet/bubbles/key"
  6. tea "github.com/charmbracelet/bubbletea"
  7. "github.com/charmbracelet/lipgloss"
  8. "github.com/sst/opencode/internal/app"
  9. "github.com/sst/opencode/internal/completions"
  10. "github.com/sst/opencode/internal/components/chat"
  11. "github.com/sst/opencode/internal/components/dialog"
  12. "github.com/sst/opencode/internal/layout"
  13. "github.com/sst/opencode/internal/state"
  14. "github.com/sst/opencode/internal/status"
  15. "github.com/sst/opencode/internal/util"
  16. "github.com/sst/opencode/pkg/client"
  17. )
  18. var ChatPage PageID = "chat"
  19. type chatPage struct {
  20. app *app.App
  21. editor layout.Container
  22. messages layout.Container
  23. layout layout.SplitPaneLayout
  24. completionDialog dialog.CompletionDialog
  25. showCompletionDialog bool
  26. }
  27. type ChatKeyMap struct {
  28. NewSession key.Binding
  29. Cancel key.Binding
  30. ToggleTools key.Binding
  31. ShowCompletionDialog key.Binding
  32. }
  33. var keyMap = ChatKeyMap{
  34. NewSession: key.NewBinding(
  35. key.WithKeys("ctrl+n"),
  36. key.WithHelp("ctrl+n", "new session"),
  37. ),
  38. Cancel: key.NewBinding(
  39. key.WithKeys("esc"),
  40. key.WithHelp("esc", "cancel"),
  41. ),
  42. ToggleTools: key.NewBinding(
  43. key.WithKeys("ctrl+h"),
  44. key.WithHelp("ctrl+h", "toggle tools"),
  45. ),
  46. ShowCompletionDialog: key.NewBinding(
  47. key.WithKeys("/"),
  48. key.WithHelp("/", "Complete"),
  49. ),
  50. }
  51. func (p *chatPage) Init() tea.Cmd {
  52. cmds := []tea.Cmd{
  53. p.layout.Init(),
  54. }
  55. cmds = append(cmds, p.completionDialog.Init())
  56. return tea.Batch(cmds...)
  57. }
  58. func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  59. var cmds []tea.Cmd
  60. switch msg := msg.(type) {
  61. case tea.WindowSizeMsg:
  62. cmd := p.layout.SetSize(msg.Width, msg.Height)
  63. cmds = append(cmds, cmd)
  64. case chat.SendMsg:
  65. cmd := p.sendMessage(msg.Text, msg.Attachments)
  66. if cmd != nil {
  67. return p, cmd
  68. }
  69. case dialog.CommandRunCustomMsg:
  70. // Check if the agent is busy before executing custom commands
  71. if p.app.IsBusy() {
  72. status.Warn("Agent is busy, please wait before executing a command...")
  73. return p, nil
  74. }
  75. // Process the command content with arguments if any
  76. content := msg.Content
  77. if msg.Args != nil {
  78. // Replace all named arguments with their values
  79. for name, value := range msg.Args {
  80. placeholder := "$" + name
  81. content = strings.ReplaceAll(content, placeholder, value)
  82. }
  83. }
  84. // Handle custom command execution
  85. cmd := p.sendMessage(content, nil)
  86. if cmd != nil {
  87. return p, cmd
  88. }
  89. case state.SessionSelectedMsg:
  90. cmd := p.setSidebar()
  91. cmds = append(cmds, cmd)
  92. case state.SessionClearedMsg:
  93. cmd := p.setSidebar()
  94. cmds = append(cmds, cmd)
  95. case dialog.CompletionDialogCloseMsg:
  96. p.showCompletionDialog = false
  97. p.app.SetCompletionDialogOpen(false)
  98. case tea.KeyMsg:
  99. switch {
  100. case key.Matches(msg, keyMap.ShowCompletionDialog):
  101. p.showCompletionDialog = true
  102. p.app.SetCompletionDialogOpen(true)
  103. // Continue sending keys to layout->chat
  104. case key.Matches(msg, keyMap.NewSession):
  105. p.app.Session = &client.SessionInfo{}
  106. p.app.Messages = []client.MessageInfo{}
  107. return p, tea.Batch(
  108. p.clearSidebar(),
  109. util.CmdHandler(state.SessionClearedMsg{}),
  110. )
  111. case key.Matches(msg, keyMap.Cancel):
  112. if p.app.Session.Id != "" {
  113. // Cancel the current session's generation process
  114. // This allows users to interrupt long-running operations
  115. p.app.Cancel(context.Background(), p.app.Session.Id)
  116. return p, nil
  117. }
  118. case key.Matches(msg, keyMap.ToggleTools):
  119. return p, util.CmdHandler(chat.ToggleToolMessagesMsg{})
  120. }
  121. }
  122. if p.showCompletionDialog {
  123. context, contextCmd := p.completionDialog.Update(msg)
  124. p.completionDialog = context.(dialog.CompletionDialog)
  125. cmds = append(cmds, contextCmd)
  126. // Doesn't forward event if enter key is pressed
  127. if keyMsg, ok := msg.(tea.KeyMsg); ok {
  128. if keyMsg.String() == "enter" {
  129. return p, tea.Batch(cmds...)
  130. }
  131. }
  132. }
  133. u, cmd := p.layout.Update(msg)
  134. cmds = append(cmds, cmd)
  135. p.layout = u.(layout.SplitPaneLayout)
  136. return p, tea.Batch(cmds...)
  137. }
  138. func (p *chatPage) setSidebar() tea.Cmd {
  139. sidebarContainer := layout.NewContainer(
  140. chat.NewSidebarCmp(p.app),
  141. layout.WithPadding(1, 1, 1, 1),
  142. )
  143. return tea.Batch(p.layout.SetRightPanel(sidebarContainer), sidebarContainer.Init())
  144. }
  145. func (p *chatPage) clearSidebar() tea.Cmd {
  146. return p.layout.ClearRightPanel()
  147. }
  148. func (p *chatPage) sendMessage(text string, attachments []app.Attachment) tea.Cmd {
  149. var cmds []tea.Cmd
  150. cmd := p.app.SendChatMessage(context.Background(), text, attachments)
  151. cmds = append(cmds, cmd)
  152. cmd = p.setSidebar()
  153. if cmd != nil {
  154. cmds = append(cmds, cmd)
  155. }
  156. return tea.Batch(cmds...)
  157. }
  158. func (p *chatPage) SetSize(width, height int) tea.Cmd {
  159. return p.layout.SetSize(width, height)
  160. }
  161. func (p *chatPage) GetSize() (int, int) {
  162. return p.layout.GetSize()
  163. }
  164. func (p *chatPage) View() string {
  165. layoutView := p.layout.View()
  166. if p.showCompletionDialog {
  167. _, layoutHeight := p.layout.GetSize()
  168. editorWidth, editorHeight := p.editor.GetSize()
  169. p.completionDialog.SetWidth(editorWidth)
  170. overlay := p.completionDialog.View()
  171. layoutView = layout.PlaceOverlay(
  172. 0,
  173. layoutHeight-editorHeight-lipgloss.Height(overlay),
  174. overlay,
  175. layoutView,
  176. false,
  177. )
  178. }
  179. return layoutView
  180. }
  181. func (p *chatPage) BindingKeys() []key.Binding {
  182. bindings := layout.KeyMapToSlice(keyMap)
  183. bindings = append(bindings, p.messages.BindingKeys()...)
  184. bindings = append(bindings, p.editor.BindingKeys()...)
  185. return bindings
  186. }
  187. func NewChatPage(app *app.App) tea.Model {
  188. cg := completions.NewFileAndFolderContextGroup()
  189. completionDialog := dialog.NewCompletionDialogCmp(cg)
  190. messagesContainer := layout.NewContainer(
  191. chat.NewMessagesCmp(app),
  192. layout.WithPadding(1, 1, 0, 1),
  193. )
  194. editorContainer := layout.NewContainer(
  195. chat.NewEditorCmp(app),
  196. layout.WithBorder(true, false, false, false),
  197. )
  198. return &chatPage{
  199. app: app,
  200. editor: editorContainer,
  201. messages: messagesContainer,
  202. completionDialog: completionDialog,
  203. layout: layout.NewSplitPane(
  204. layout.WithLeftPanel(messagesContainer),
  205. layout.WithBottomPanel(editorContainer),
  206. ),
  207. }
  208. }