sidebar.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. package chat
  2. import (
  3. "fmt"
  4. "sort"
  5. "strings"
  6. tea "github.com/charmbracelet/bubbletea"
  7. "github.com/charmbracelet/lipgloss"
  8. "github.com/sst/opencode/internal/app"
  9. "github.com/sst/opencode/internal/state"
  10. "github.com/sst/opencode/internal/styles"
  11. "github.com/sst/opencode/internal/theme"
  12. )
  13. type sidebarCmp struct {
  14. app *app.App
  15. width, height int
  16. modFiles map[string]struct {
  17. additions int
  18. removals int
  19. }
  20. }
  21. func (m *sidebarCmp) Init() tea.Cmd {
  22. // TODO: History service not implemented in API yet
  23. // Initialize the modified files map
  24. m.modFiles = make(map[string]struct {
  25. additions int
  26. removals int
  27. })
  28. return nil
  29. }
  30. func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  31. switch msg.(type) {
  32. case state.SessionSelectedMsg:
  33. // TODO: History service not implemented in API yet
  34. // ctx := context.Background()
  35. // m.loadModifiedFiles(ctx)
  36. // case pubsub.Event[history.File]:
  37. // TODO: History service not implemented in API yet
  38. // if msg.Payload.SessionID == m.app.CurrentSession.ID {
  39. // // Process the individual file change instead of reloading all files
  40. // ctx := context.Background()
  41. // m.processFileChanges(ctx, msg.Payload)
  42. // }
  43. }
  44. return m, nil
  45. }
  46. func (m *sidebarCmp) View() string {
  47. t := theme.CurrentTheme()
  48. baseStyle := styles.BaseStyle()
  49. shareUrl := ""
  50. if m.app.Session.Share != nil {
  51. shareUrl = baseStyle.Foreground(t.TextMuted()).Render(m.app.Session.Share.Url)
  52. }
  53. // qrcode := ""
  54. // if m.app.Session.ShareID != nil {
  55. // url := "https://dev.opencode.ai/share?id="
  56. // qrcode, _, _ = qr.Generate(url + m.app.Session.Id)
  57. // }
  58. return baseStyle.
  59. Width(m.width).
  60. PaddingLeft(4).
  61. PaddingRight(1).
  62. Render(
  63. lipgloss.JoinVertical(
  64. lipgloss.Top,
  65. header(m.app, m.width),
  66. " ",
  67. m.sessionSection(),
  68. shareUrl,
  69. ),
  70. )
  71. }
  72. func (m *sidebarCmp) sessionSection() string {
  73. t := theme.CurrentTheme()
  74. baseStyle := styles.BaseStyle()
  75. sessionKey := baseStyle.
  76. Foreground(t.Primary()).
  77. Bold(true).
  78. Render("Session")
  79. sessionValue := baseStyle.
  80. Foreground(t.Text()).
  81. Render(fmt.Sprintf(": %s", m.app.Session.Title))
  82. return sessionKey + sessionValue
  83. }
  84. func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string {
  85. t := theme.CurrentTheme()
  86. baseStyle := styles.BaseStyle()
  87. stats := ""
  88. if additions > 0 && removals > 0 {
  89. additionsStr := baseStyle.
  90. Foreground(t.Success()).
  91. PaddingLeft(1).
  92. Render(fmt.Sprintf("+%d", additions))
  93. removalsStr := baseStyle.
  94. Foreground(t.Error()).
  95. PaddingLeft(1).
  96. Render(fmt.Sprintf("-%d", removals))
  97. content := lipgloss.JoinHorizontal(lipgloss.Left, additionsStr, removalsStr)
  98. stats = baseStyle.Width(lipgloss.Width(content)).Render(content)
  99. } else if additions > 0 {
  100. additionsStr := fmt.Sprintf(" %s", baseStyle.
  101. PaddingLeft(1).
  102. Foreground(t.Success()).
  103. Render(fmt.Sprintf("+%d", additions)))
  104. stats = baseStyle.Width(lipgloss.Width(additionsStr)).Render(additionsStr)
  105. } else if removals > 0 {
  106. removalsStr := fmt.Sprintf(" %s", baseStyle.
  107. PaddingLeft(1).
  108. Foreground(t.Error()).
  109. Render(fmt.Sprintf("-%d", removals)))
  110. stats = baseStyle.Width(lipgloss.Width(removalsStr)).Render(removalsStr)
  111. }
  112. filePathStr := baseStyle.Render(filePath)
  113. return baseStyle.
  114. Width(m.width).
  115. Render(
  116. lipgloss.JoinHorizontal(
  117. lipgloss.Left,
  118. filePathStr,
  119. stats,
  120. ),
  121. )
  122. }
  123. func (m *sidebarCmp) modifiedFiles() string {
  124. t := theme.CurrentTheme()
  125. baseStyle := styles.BaseStyle()
  126. modifiedFiles := baseStyle.
  127. Width(m.width).
  128. Foreground(t.Primary()).
  129. Bold(true).
  130. Render("Modified Files:")
  131. // If no modified files, show a placeholder message
  132. if m.modFiles == nil || len(m.modFiles) == 0 {
  133. message := "No modified files"
  134. remainingWidth := m.width - lipgloss.Width(message)
  135. if remainingWidth > 0 {
  136. message += strings.Repeat(" ", remainingWidth)
  137. }
  138. return baseStyle.
  139. Width(m.width).
  140. Render(
  141. lipgloss.JoinVertical(
  142. lipgloss.Top,
  143. modifiedFiles,
  144. baseStyle.Foreground(t.TextMuted()).Render(message),
  145. ),
  146. )
  147. }
  148. // Sort file paths alphabetically for consistent ordering
  149. var paths []string
  150. for path := range m.modFiles {
  151. paths = append(paths, path)
  152. }
  153. sort.Strings(paths)
  154. // Create views for each file in sorted order
  155. var fileViews []string
  156. for _, path := range paths {
  157. stats := m.modFiles[path]
  158. fileViews = append(fileViews, m.modifiedFile(path, stats.additions, stats.removals))
  159. }
  160. return baseStyle.
  161. Width(m.width).
  162. Render(
  163. lipgloss.JoinVertical(
  164. lipgloss.Top,
  165. modifiedFiles,
  166. lipgloss.JoinVertical(
  167. lipgloss.Left,
  168. fileViews...,
  169. ),
  170. ),
  171. )
  172. }
  173. func (m *sidebarCmp) SetSize(width, height int) tea.Cmd {
  174. m.width = width
  175. m.height = height
  176. return nil
  177. }
  178. func (m *sidebarCmp) GetSize() (int, int) {
  179. return m.width, m.height
  180. }
  181. func NewSidebarCmp(app *app.App) tea.Model {
  182. return &sidebarCmp{
  183. app: app,
  184. }
  185. }