chat.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. package page
  2. import (
  3. "context"
  4. "github.com/charmbracelet/bubbles/key"
  5. tea "github.com/charmbracelet/bubbletea"
  6. "github.com/opencode-ai/opencode/internal/app"
  7. "github.com/opencode-ai/opencode/internal/session"
  8. "github.com/opencode-ai/opencode/internal/tui/components/chat"
  9. "github.com/opencode-ai/opencode/internal/tui/layout"
  10. "github.com/opencode-ai/opencode/internal/tui/util"
  11. )
  12. var ChatPage PageID = "chat"
  13. type chatPage struct {
  14. app *app.App
  15. editor layout.Container
  16. messages layout.Container
  17. layout layout.SplitPaneLayout
  18. session session.Session
  19. }
  20. type ChatKeyMap struct {
  21. NewSession key.Binding
  22. Cancel key.Binding
  23. }
  24. var keyMap = ChatKeyMap{
  25. NewSession: key.NewBinding(
  26. key.WithKeys("ctrl+n"),
  27. key.WithHelp("ctrl+n", "new session"),
  28. ),
  29. Cancel: key.NewBinding(
  30. key.WithKeys("esc"),
  31. key.WithHelp("esc", "cancel"),
  32. ),
  33. }
  34. func (p *chatPage) Init() tea.Cmd {
  35. cmds := []tea.Cmd{
  36. p.layout.Init(),
  37. }
  38. return tea.Batch(cmds...)
  39. }
  40. func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  41. var cmds []tea.Cmd
  42. switch msg := msg.(type) {
  43. case tea.WindowSizeMsg:
  44. cmd := p.layout.SetSize(msg.Width, msg.Height)
  45. cmds = append(cmds, cmd)
  46. case chat.SendMsg:
  47. cmd := p.sendMessage(msg.Text)
  48. if cmd != nil {
  49. return p, cmd
  50. }
  51. case chat.SessionSelectedMsg:
  52. if p.session.ID == "" {
  53. cmd := p.setSidebar()
  54. if cmd != nil {
  55. cmds = append(cmds, cmd)
  56. }
  57. }
  58. p.session = msg
  59. case tea.KeyMsg:
  60. switch {
  61. case key.Matches(msg, keyMap.NewSession):
  62. p.session = session.Session{}
  63. return p, tea.Batch(
  64. p.clearSidebar(),
  65. util.CmdHandler(chat.SessionClearedMsg{}),
  66. )
  67. case key.Matches(msg, keyMap.Cancel):
  68. if p.session.ID != "" {
  69. // Cancel the current session's generation process
  70. // This allows users to interrupt long-running operations
  71. p.app.CoderAgent.Cancel(p.session.ID)
  72. return p, nil
  73. }
  74. }
  75. }
  76. u, cmd := p.layout.Update(msg)
  77. cmds = append(cmds, cmd)
  78. p.layout = u.(layout.SplitPaneLayout)
  79. return p, tea.Batch(cmds...)
  80. }
  81. func (p *chatPage) setSidebar() tea.Cmd {
  82. sidebarContainer := layout.NewContainer(
  83. chat.NewSidebarCmp(p.session, p.app.History),
  84. layout.WithPadding(1, 1, 1, 1),
  85. )
  86. return tea.Batch(p.layout.SetRightPanel(sidebarContainer), sidebarContainer.Init())
  87. }
  88. func (p *chatPage) clearSidebar() tea.Cmd {
  89. return p.layout.ClearRightPanel()
  90. }
  91. func (p *chatPage) sendMessage(text string) tea.Cmd {
  92. var cmds []tea.Cmd
  93. if p.session.ID == "" {
  94. session, err := p.app.Sessions.Create(context.Background(), "New Session")
  95. if err != nil {
  96. return util.ReportError(err)
  97. }
  98. p.session = session
  99. cmd := p.setSidebar()
  100. if cmd != nil {
  101. cmds = append(cmds, cmd)
  102. }
  103. cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
  104. }
  105. p.app.CoderAgent.Run(context.Background(), p.session.ID, text)
  106. return tea.Batch(cmds...)
  107. }
  108. func (p *chatPage) SetSize(width, height int) tea.Cmd {
  109. return p.layout.SetSize(width, height)
  110. }
  111. func (p *chatPage) GetSize() (int, int) {
  112. return p.layout.GetSize()
  113. }
  114. func (p *chatPage) View() string {
  115. return p.layout.View()
  116. }
  117. func (p *chatPage) BindingKeys() []key.Binding {
  118. bindings := layout.KeyMapToSlice(keyMap)
  119. bindings = append(bindings, p.messages.BindingKeys()...)
  120. return bindings
  121. }
  122. func NewChatPage(app *app.App) tea.Model {
  123. messagesContainer := layout.NewContainer(
  124. chat.NewMessagesCmp(app),
  125. layout.WithPadding(1, 1, 0, 1),
  126. )
  127. editorContainer := layout.NewContainer(
  128. chat.NewEditorCmp(app),
  129. layout.WithBorder(true, false, false, false),
  130. )
  131. return &chatPage{
  132. app: app,
  133. editor: editorContainer,
  134. messages: messagesContainer,
  135. layout: layout.NewSplitPane(
  136. layout.WithLeftPanel(messagesContainer),
  137. layout.WithBottomPanel(editorContainer),
  138. ),
  139. }
  140. }