sidebar.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. package chat
  2. import (
  3. "context"
  4. "fmt"
  5. "sort"
  6. "strings"
  7. tea "github.com/charmbracelet/bubbletea"
  8. "github.com/charmbracelet/lipgloss"
  9. "github.com/sst/opencode/internal/config"
  10. "github.com/sst/opencode/internal/tui/app"
  11. // "github.com/sst/opencode/internal/diff"
  12. "github.com/sst/opencode/internal/history"
  13. "github.com/sst/opencode/internal/pubsub"
  14. "github.com/sst/opencode/internal/tui/state"
  15. "github.com/sst/opencode/internal/tui/styles"
  16. "github.com/sst/opencode/internal/tui/theme"
  17. )
  18. type sidebarCmp struct {
  19. app *app.App
  20. width, height int
  21. modFiles map[string]struct {
  22. additions int
  23. removals int
  24. }
  25. }
  26. func (m *sidebarCmp) Init() tea.Cmd {
  27. // TODO: History service not implemented in API yet
  28. // Initialize the modified files map
  29. m.modFiles = make(map[string]struct {
  30. additions int
  31. removals int
  32. })
  33. return nil
  34. }
  35. func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  36. switch msg.(type) {
  37. case state.SessionSelectedMsg:
  38. // TODO: History service not implemented in API yet
  39. // ctx := context.Background()
  40. // m.loadModifiedFiles(ctx)
  41. case pubsub.Event[history.File]:
  42. // TODO: History service not implemented in API yet
  43. // if msg.Payload.SessionID == m.app.CurrentSession.ID {
  44. // // Process the individual file change instead of reloading all files
  45. // ctx := context.Background()
  46. // m.processFileChanges(ctx, msg.Payload)
  47. // }
  48. }
  49. return m, nil
  50. }
  51. func (m *sidebarCmp) View() string {
  52. baseStyle := styles.BaseStyle()
  53. return baseStyle.
  54. Width(m.width).
  55. PaddingLeft(4).
  56. PaddingRight(1).
  57. Render(
  58. lipgloss.JoinVertical(
  59. lipgloss.Top,
  60. header(m.width),
  61. " ",
  62. m.sessionSection(),
  63. " ",
  64. lspsConfigured(m.width),
  65. " ",
  66. m.modifiedFiles(),
  67. ),
  68. )
  69. }
  70. func (m *sidebarCmp) sessionSection() string {
  71. t := theme.CurrentTheme()
  72. baseStyle := styles.BaseStyle()
  73. sessionKey := baseStyle.
  74. Foreground(t.Primary()).
  75. Bold(true).
  76. Render("Session")
  77. sessionValue := baseStyle.
  78. Foreground(t.Text()).
  79. Render(fmt.Sprintf(": %s", m.app.CurrentSession.Title))
  80. return sessionKey + sessionValue
  81. }
  82. func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string {
  83. t := theme.CurrentTheme()
  84. baseStyle := styles.BaseStyle()
  85. stats := ""
  86. if additions > 0 && removals > 0 {
  87. additionsStr := baseStyle.
  88. Foreground(t.Success()).
  89. PaddingLeft(1).
  90. Render(fmt.Sprintf("+%d", additions))
  91. removalsStr := baseStyle.
  92. Foreground(t.Error()).
  93. PaddingLeft(1).
  94. Render(fmt.Sprintf("-%d", removals))
  95. content := lipgloss.JoinHorizontal(lipgloss.Left, additionsStr, removalsStr)
  96. stats = baseStyle.Width(lipgloss.Width(content)).Render(content)
  97. } else if additions > 0 {
  98. additionsStr := fmt.Sprintf(" %s", baseStyle.
  99. PaddingLeft(1).
  100. Foreground(t.Success()).
  101. Render(fmt.Sprintf("+%d", additions)))
  102. stats = baseStyle.Width(lipgloss.Width(additionsStr)).Render(additionsStr)
  103. } else if removals > 0 {
  104. removalsStr := fmt.Sprintf(" %s", baseStyle.
  105. PaddingLeft(1).
  106. Foreground(t.Error()).
  107. Render(fmt.Sprintf("-%d", removals)))
  108. stats = baseStyle.Width(lipgloss.Width(removalsStr)).Render(removalsStr)
  109. }
  110. filePathStr := baseStyle.Render(filePath)
  111. return baseStyle.
  112. Width(m.width).
  113. Render(
  114. lipgloss.JoinHorizontal(
  115. lipgloss.Left,
  116. filePathStr,
  117. stats,
  118. ),
  119. )
  120. }
  121. func (m *sidebarCmp) modifiedFiles() string {
  122. t := theme.CurrentTheme()
  123. baseStyle := styles.BaseStyle()
  124. modifiedFiles := baseStyle.
  125. Width(m.width).
  126. Foreground(t.Primary()).
  127. Bold(true).
  128. Render("Modified Files:")
  129. // If no modified files, show a placeholder message
  130. if m.modFiles == nil || len(m.modFiles) == 0 {
  131. message := "No modified files"
  132. remainingWidth := m.width - lipgloss.Width(message)
  133. if remainingWidth > 0 {
  134. message += strings.Repeat(" ", remainingWidth)
  135. }
  136. return baseStyle.
  137. Width(m.width).
  138. Render(
  139. lipgloss.JoinVertical(
  140. lipgloss.Top,
  141. modifiedFiles,
  142. baseStyle.Foreground(t.TextMuted()).Render(message),
  143. ),
  144. )
  145. }
  146. // Sort file paths alphabetically for consistent ordering
  147. var paths []string
  148. for path := range m.modFiles {
  149. paths = append(paths, path)
  150. }
  151. sort.Strings(paths)
  152. // Create views for each file in sorted order
  153. var fileViews []string
  154. for _, path := range paths {
  155. stats := m.modFiles[path]
  156. fileViews = append(fileViews, m.modifiedFile(path, stats.additions, stats.removals))
  157. }
  158. return baseStyle.
  159. Width(m.width).
  160. Render(
  161. lipgloss.JoinVertical(
  162. lipgloss.Top,
  163. modifiedFiles,
  164. lipgloss.JoinVertical(
  165. lipgloss.Left,
  166. fileViews...,
  167. ),
  168. ),
  169. )
  170. }
  171. func (m *sidebarCmp) SetSize(width, height int) tea.Cmd {
  172. m.width = width
  173. m.height = height
  174. return nil
  175. }
  176. func (m *sidebarCmp) GetSize() (int, int) {
  177. return m.width, m.height
  178. }
  179. func NewSidebarCmp(app *app.App) tea.Model {
  180. return &sidebarCmp{
  181. app: app,
  182. }
  183. }
  184. func (m *sidebarCmp) loadModifiedFiles(ctx context.Context) {
  185. if m.app.CurrentSession.ID == "" {
  186. return
  187. }
  188. // TODO: History service not implemented in API yet
  189. return
  190. /*
  191. // Get all latest files for this session
  192. latestFiles, err := m.app.History.ListLatestSessionFiles(ctx, m.app.CurrentSession.ID)
  193. if err != nil {
  194. return
  195. }
  196. // Get all files for this session (to find initial versions)
  197. allFiles, err := m.app.History.ListBySession(ctx, m.app.CurrentSession.ID)
  198. if err != nil {
  199. return
  200. }
  201. */
  202. // Clear the existing map to rebuild it
  203. m.modFiles = make(map[string]struct {
  204. additions int
  205. removals int
  206. })
  207. /*
  208. // Process each latest file
  209. for _, file := range latestFiles {
  210. // Skip if this is the initial version (no changes to show)
  211. if file.Version == history.InitialVersion {
  212. continue
  213. }
  214. // Find the initial version for this specific file
  215. var initialVersion history.File
  216. for _, v := range allFiles {
  217. if v.Path == file.Path && v.Version == history.InitialVersion {
  218. initialVersion = v
  219. break
  220. }
  221. }
  222. // Skip if we can't find the initial version
  223. if initialVersion.ID == "" {
  224. continue
  225. }
  226. if initialVersion.Content == file.Content {
  227. continue
  228. }
  229. // Calculate diff between initial and latest version
  230. _, additions, removals := diff.GenerateDiff(initialVersion.Content, file.Content, file.Path)
  231. // Only add to modified files if there are changes
  232. if additions > 0 || removals > 0 {
  233. // Remove working directory prefix from file path
  234. displayPath := file.Path
  235. workingDir := config.WorkingDirectory()
  236. displayPath = strings.TrimPrefix(displayPath, workingDir)
  237. displayPath = strings.TrimPrefix(displayPath, "/")
  238. m.modFiles[displayPath] = struct {
  239. additions int
  240. removals int
  241. }{
  242. additions: additions,
  243. removals: removals,
  244. }
  245. }
  246. }
  247. */
  248. }
  249. func (m *sidebarCmp) processFileChanges(ctx context.Context, file history.File) {
  250. // TODO: History service not implemented in API yet
  251. return
  252. /*
  253. // Skip if this is the initial version (no changes to show)
  254. if file.Version == history.InitialVersion {
  255. return
  256. }
  257. // Find the initial version for this file
  258. initialVersion, err := m.findInitialVersion(ctx, file.Path)
  259. if err != nil || initialVersion.ID == "" {
  260. return
  261. }
  262. // Skip if content hasn't changed
  263. if initialVersion.Content == file.Content {
  264. // If this file was previously modified but now matches the initial version,
  265. // remove it from the modified files list
  266. displayPath := getDisplayPath(file.Path)
  267. delete(m.modFiles, displayPath)
  268. return
  269. }
  270. // Calculate diff between initial and latest version
  271. _, additions, removals := diff.GenerateDiff(initialVersion.Content, file.Content, file.Path)
  272. // Only add to modified files if there are changes
  273. if additions > 0 || removals > 0 {
  274. displayPath := getDisplayPath(file.Path)
  275. m.modFiles[displayPath] = struct {
  276. additions int
  277. removals int
  278. }{
  279. additions: additions,
  280. removals: removals,
  281. }
  282. } else {
  283. // If no changes, remove from modified files
  284. displayPath := getDisplayPath(file.Path)
  285. delete(m.modFiles, displayPath)
  286. }
  287. */
  288. }
  289. // Helper function to find the initial version of a file
  290. func (m *sidebarCmp) findInitialVersion(ctx context.Context, path string) (history.File, error) {
  291. // TODO: History service not implemented in API yet
  292. return history.File{}, fmt.Errorf("history service not implemented")
  293. /*
  294. // Get all versions of this file for the session
  295. fileVersions, err := m.app.History.ListBySession(ctx, m.app.CurrentSession.ID)
  296. if err != nil {
  297. return history.File{}, err
  298. }
  299. */
  300. /*
  301. // Find the initial version
  302. for _, v := range fileVersions {
  303. if v.Path == path && v.Version == history.InitialVersion {
  304. return v, nil
  305. }
  306. }
  307. return history.File{}, fmt.Errorf("initial version not found")
  308. */
  309. }
  310. // Helper function to get the display path for a file
  311. func getDisplayPath(path string) string {
  312. workingDir := config.WorkingDirectory()
  313. displayPath := strings.TrimPrefix(path, workingDir)
  314. return strings.TrimPrefix(displayPath, "/")
  315. }