init.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. package init
  2. import (
  3. "github.com/charmbracelet/bubbles/v2/key"
  4. tea "github.com/charmbracelet/bubbletea/v2"
  5. "github.com/charmbracelet/lipgloss/v2"
  6. "github.com/charmbracelet/crush/internal/config"
  7. cmpChat "github.com/charmbracelet/crush/internal/tui/components/chat"
  8. "github.com/charmbracelet/crush/internal/tui/components/core"
  9. "github.com/charmbracelet/crush/internal/tui/components/dialogs"
  10. "github.com/charmbracelet/crush/internal/tui/styles"
  11. "github.com/charmbracelet/crush/internal/tui/util"
  12. )
  13. const InitDialogID dialogs.DialogID = "init"
  14. // InitDialogCmp is a component that asks the user if they want to initialize the project.
  15. type InitDialogCmp interface {
  16. dialogs.DialogModel
  17. }
  18. type initDialogCmp struct {
  19. wWidth, wHeight int
  20. width, height int
  21. selected int
  22. keyMap KeyMap
  23. }
  24. // NewInitDialogCmp creates a new InitDialogCmp.
  25. func NewInitDialogCmp() InitDialogCmp {
  26. return &initDialogCmp{
  27. selected: 0,
  28. keyMap: DefaultKeyMap(),
  29. }
  30. }
  31. // Init implements tea.Model.
  32. func (m *initDialogCmp) Init() tea.Cmd {
  33. return nil
  34. }
  35. // Update implements tea.Model.
  36. func (m *initDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  37. switch msg := msg.(type) {
  38. case tea.WindowSizeMsg:
  39. m.wWidth = msg.Width
  40. m.wHeight = msg.Height
  41. cmd := m.SetSize()
  42. return m, cmd
  43. case tea.KeyPressMsg:
  44. switch {
  45. case key.Matches(msg, m.keyMap.Close):
  46. return m, tea.Batch(
  47. util.CmdHandler(dialogs.CloseDialogMsg{}),
  48. m.handleInitialization(false),
  49. )
  50. case key.Matches(msg, m.keyMap.ChangeSelection):
  51. m.selected = (m.selected + 1) % 2
  52. return m, nil
  53. case key.Matches(msg, m.keyMap.Select):
  54. return m, tea.Batch(
  55. util.CmdHandler(dialogs.CloseDialogMsg{}),
  56. m.handleInitialization(m.selected == 0),
  57. )
  58. case key.Matches(msg, m.keyMap.Y):
  59. return m, tea.Batch(
  60. util.CmdHandler(dialogs.CloseDialogMsg{}),
  61. m.handleInitialization(true),
  62. )
  63. case key.Matches(msg, m.keyMap.N):
  64. return m, tea.Batch(
  65. util.CmdHandler(dialogs.CloseDialogMsg{}),
  66. m.handleInitialization(false),
  67. )
  68. }
  69. }
  70. return m, nil
  71. }
  72. func (m *initDialogCmp) renderButtons() string {
  73. t := styles.CurrentTheme()
  74. baseStyle := t.S().Base
  75. buttons := []core.ButtonOpts{
  76. {
  77. Text: "Yes",
  78. UnderlineIndex: 0, // "Y"
  79. Selected: m.selected == 0,
  80. },
  81. {
  82. Text: "No",
  83. UnderlineIndex: 0, // "N"
  84. Selected: m.selected == 1,
  85. },
  86. }
  87. content := core.SelectableButtons(buttons, " ")
  88. return baseStyle.AlignHorizontal(lipgloss.Right).Width(m.width - 4).Render(content)
  89. }
  90. func (m *initDialogCmp) renderContent() string {
  91. t := styles.CurrentTheme()
  92. baseStyle := t.S().Base
  93. explanation := t.S().Text.
  94. Width(m.width - 4).
  95. Render("Initialization generates a new CRUSH.md file that contains information about your codebase, this file serves as memory for each project, you can freely add to it to help the agents be better at their job.")
  96. question := t.S().Text.
  97. Width(m.width - 4).
  98. Render("Would you like to initialize this project?")
  99. return baseStyle.Render(lipgloss.JoinVertical(
  100. lipgloss.Left,
  101. explanation,
  102. "",
  103. question,
  104. ))
  105. }
  106. func (m *initDialogCmp) render() string {
  107. t := styles.CurrentTheme()
  108. baseStyle := t.S().Base
  109. title := core.Title("Initialize Project", m.width-4)
  110. content := m.renderContent()
  111. buttons := m.renderButtons()
  112. dialogContent := lipgloss.JoinVertical(
  113. lipgloss.Top,
  114. title,
  115. "",
  116. content,
  117. "",
  118. buttons,
  119. "",
  120. )
  121. return baseStyle.
  122. Padding(0, 1).
  123. Border(lipgloss.RoundedBorder()).
  124. BorderForeground(t.BorderFocus).
  125. Width(m.width).
  126. Render(dialogContent)
  127. }
  128. // View implements tea.Model.
  129. func (m *initDialogCmp) View() tea.View {
  130. return tea.NewView(m.render())
  131. }
  132. // SetSize sets the size of the component.
  133. func (m *initDialogCmp) SetSize() tea.Cmd {
  134. m.width = min(90, m.wWidth)
  135. m.height = min(15, m.wHeight)
  136. return nil
  137. }
  138. // ID implements DialogModel.
  139. func (m *initDialogCmp) ID() dialogs.DialogID {
  140. return InitDialogID
  141. }
  142. // Position implements DialogModel.
  143. func (m *initDialogCmp) Position() (int, int) {
  144. row := (m.wHeight / 2) - (m.height / 2)
  145. col := (m.wWidth / 2) - (m.width / 2)
  146. return row, col
  147. }
  148. // handleInitialization handles the initialization logic when the dialog is closed.
  149. func (m *initDialogCmp) handleInitialization(initialize bool) tea.Cmd {
  150. if initialize {
  151. // Run the initialization command
  152. prompt := `Please analyze this codebase and create a CRUSH.md file containing:
  153. 1. Build/lint/test commands - especially for running a single test
  154. 2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
  155. The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
  156. If there's already a CRUSH.md, improve it.
  157. If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
  158. Add the .crush directory to the .gitignore file if it's not already there.`
  159. // Mark the project as initialized
  160. if err := config.MarkProjectInitialized(); err != nil {
  161. return util.ReportError(err)
  162. }
  163. return tea.Sequence(
  164. util.CmdHandler(cmpChat.SessionClearedMsg{}),
  165. util.CmdHandler(cmpChat.SendMsg{
  166. Text: prompt,
  167. }),
  168. )
  169. } else {
  170. // Mark the project as initialized without running the command
  171. if err := config.MarkProjectInitialized(); err != nil {
  172. return util.ReportError(err)
  173. }
  174. }
  175. return nil
  176. }
  177. // CloseInitDialogMsg is a message that is sent when the init dialog is closed.
  178. type CloseInitDialogMsg struct {
  179. Initialize bool
  180. }
  181. // ShowInitDialogMsg is a message that is sent to show the init dialog.
  182. type ShowInitDialogMsg struct {
  183. Show bool
  184. }