chat.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. package page
  2. import (
  3. "context"
  4. "github.com/charmbracelet/bubbles/v2/key"
  5. tea "github.com/charmbracelet/bubbletea/v2"
  6. "github.com/charmbracelet/lipgloss/v2"
  7. "github.com/sst/opencode/internal/app"
  8. "github.com/sst/opencode/internal/completions"
  9. "github.com/sst/opencode/internal/components/chat"
  10. "github.com/sst/opencode/internal/components/dialog"
  11. "github.com/sst/opencode/internal/layout"
  12. "github.com/sst/opencode/internal/util"
  13. )
  14. var ChatPage PageID = "chat"
  15. type chatPage struct {
  16. app *app.App
  17. editor layout.Container
  18. messages layout.Container
  19. layout layout.FlexLayout
  20. completionDialog dialog.CompletionDialog
  21. completionManager *completions.CompletionManager
  22. showCompletionDialog bool
  23. }
  24. type ChatKeyMap struct {
  25. Cancel key.Binding
  26. ToggleTools key.Binding
  27. ShowCompletionDialog key.Binding
  28. }
  29. var keyMap = ChatKeyMap{
  30. Cancel: key.NewBinding(
  31. key.WithKeys("esc"),
  32. key.WithHelp("esc", "cancel"),
  33. ),
  34. ToggleTools: key.NewBinding(
  35. key.WithKeys("ctrl+h"),
  36. key.WithHelp("ctrl+h", "toggle tools"),
  37. ),
  38. ShowCompletionDialog: key.NewBinding(
  39. key.WithKeys("/"),
  40. key.WithHelp("/", "Complete"),
  41. ),
  42. }
  43. func (p *chatPage) Init() tea.Cmd {
  44. cmds := []tea.Cmd{
  45. p.layout.Init(),
  46. }
  47. cmds = append(cmds, p.completionDialog.Init())
  48. return tea.Batch(cmds...)
  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. p.showCompletionDialog = false
  58. cmd := p.sendMessage(msg.Text, msg.Attachments)
  59. if cmd != nil {
  60. return p, cmd
  61. }
  62. case dialog.CompletionDialogCloseMsg:
  63. p.showCompletionDialog = false
  64. case tea.KeyMsg:
  65. switch msg.String() {
  66. case "ctrl+c":
  67. _, cmd := p.editor.Update(msg)
  68. if cmd != nil {
  69. return p, cmd
  70. }
  71. }
  72. switch {
  73. case key.Matches(msg, keyMap.ShowCompletionDialog):
  74. p.showCompletionDialog = true
  75. // Continue sending keys to layout->chat
  76. case key.Matches(msg, keyMap.Cancel):
  77. if p.app.Session.Id != "" {
  78. // Cancel the current session's generation process
  79. // This allows users to interrupt long-running operations
  80. p.app.Cancel(context.Background(), p.app.Session.Id)
  81. return p, nil
  82. }
  83. case key.Matches(msg, keyMap.ToggleTools):
  84. return p, util.CmdHandler(chat.ToggleToolMessagesMsg{})
  85. }
  86. }
  87. if p.showCompletionDialog {
  88. // Get the current text from the editor to determine which provider to use
  89. editorModel := p.editor.GetContent().(interface{ GetValue() string })
  90. currentInput := editorModel.GetValue()
  91. provider := p.completionManager.GetProvider(currentInput)
  92. p.completionDialog.SetProvider(provider)
  93. context, contextCmd := p.completionDialog.Update(msg)
  94. p.completionDialog = context.(dialog.CompletionDialog)
  95. cmds = append(cmds, contextCmd)
  96. // Doesn't forward event if enter key is pressed
  97. if keyMsg, ok := msg.(tea.KeyMsg); ok {
  98. if keyMsg.String() == "enter" {
  99. return p, tea.Batch(cmds...)
  100. }
  101. }
  102. }
  103. u, cmd := p.layout.Update(msg)
  104. cmds = append(cmds, cmd)
  105. p.layout = u.(layout.FlexLayout)
  106. return p, tea.Batch(cmds...)
  107. }
  108. func (p *chatPage) sendMessage(text string, attachments []app.Attachment) tea.Cmd {
  109. var cmds []tea.Cmd
  110. cmd := p.app.SendChatMessage(context.Background(), text, attachments)
  111. cmds = append(cmds, cmd)
  112. return tea.Batch(cmds...)
  113. }
  114. func (p *chatPage) SetSize(width, height int) tea.Cmd {
  115. return p.layout.SetSize(width, height)
  116. }
  117. func (p *chatPage) GetSize() (int, int) {
  118. return p.layout.GetSize()
  119. }
  120. func (p *chatPage) View() string {
  121. layoutView := p.layout.View()
  122. if p.showCompletionDialog {
  123. editorWidth, _ := p.editor.GetSize()
  124. editorX, editorY := p.editor.GetPosition()
  125. p.completionDialog.SetWidth(editorWidth)
  126. overlay := p.completionDialog.View()
  127. layoutView = layout.PlaceOverlay(
  128. editorX,
  129. editorY-lipgloss.Height(overlay)+2,
  130. overlay,
  131. layoutView,
  132. )
  133. }
  134. return layoutView
  135. }
  136. func NewChatPage(app *app.App) layout.ModelWithView {
  137. completionManager := completions.NewCompletionManager(app)
  138. initialProvider := completionManager.GetProvider("")
  139. completionDialog := dialog.NewCompletionDialogComponent(initialProvider)
  140. messagesContainer := layout.NewContainer(
  141. chat.NewMessagesComponent(app),
  142. )
  143. editorContainer := layout.NewContainer(
  144. chat.NewEditorComponent(app),
  145. layout.WithMaxWidth(layout.Current.Container.Width),
  146. layout.WithAlignCenter(),
  147. )
  148. return &chatPage{
  149. app: app,
  150. editor: editorContainer,
  151. messages: messagesContainer,
  152. completionDialog: completionDialog,
  153. completionManager: completionManager,
  154. layout: layout.NewFlexLayout(
  155. layout.WithPanes(messagesContainer, editorContainer),
  156. layout.WithDirection(layout.FlexDirectionVertical),
  157. layout.WithPaneSizes(
  158. layout.FlexPaneSizeGrow,
  159. layout.FlexPaneSizeFixed(6),
  160. ),
  161. ),
  162. }
  163. }