sidebar.go 5.1 KB

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