chat.go 6.6 KB

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