1package commands
2
3import (
4 tea "github.com/charmbracelet/bubbletea/v2"
5 "github.com/charmbracelet/lipgloss/v2"
6
7 "github.com/opencode-ai/opencode/internal/tui/components/chat"
8 "github.com/opencode-ai/opencode/internal/tui/components/core/list"
9 "github.com/opencode-ai/opencode/internal/tui/components/dialogs"
10 "github.com/opencode-ai/opencode/internal/tui/styles"
11 "github.com/opencode-ai/opencode/internal/tui/theme"
12 "github.com/opencode-ai/opencode/internal/tui/util"
13)
14
15const (
16 id dialogs.DialogID = "commands"
17
18 defaultWidth int = 60
19)
20
21// Command represents a command that can be executed
22type Command struct {
23 ID string
24 Title string
25 Description string
26 Handler func(cmd Command) tea.Cmd
27}
28
29// CommandsDialog represents the commands dialog.
30type CommandsDialog interface {
31 dialogs.DialogModel
32}
33
34type commandDialogCmp struct {
35 width int
36 wWidth int // Width of the terminal window
37 wHeight int // Height of the terminal window
38
39 commandList list.ListModel
40}
41
42func NewCommandDialog() CommandsDialog {
43 commandList := list.New(list.WithFilterable(true))
44
45 return &commandDialogCmp{
46 commandList: commandList,
47 width: defaultWidth,
48 }
49}
50
51func (c *commandDialogCmp) Init() tea.Cmd {
52 commands, err := LoadCustomCommands()
53 if err != nil {
54 return util.ReportError(err)
55 }
56
57 commands = append(commands, c.defaultCommands()...)
58
59 commandItems := make([]util.Model, 0, len(commands))
60
61 for _, cmd := range commands {
62 commandItems = append(commandItems, NewCommandItem(cmd))
63 }
64
65 c.commandList.SetItems(commandItems)
66 return c.commandList.Init()
67}
68
69func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
70 switch msg := msg.(type) {
71 case tea.WindowSizeMsg:
72 c.wWidth = msg.Width
73 c.wHeight = msg.Height
74 return c, c.commandList.SetSize(c.listWidth(), c.listHeight())
75 }
76 u, cmd := c.commandList.Update(msg)
77 c.commandList = u.(list.ListModel)
78 return c, cmd
79}
80
81func (c *commandDialogCmp) View() tea.View {
82 listView := c.commandList.View()
83 v := tea.NewView(c.style().Render(listView.String()))
84 if listView.Cursor() != nil {
85 c := c.moveCursor(listView.Cursor())
86 v.SetCursor(c)
87 }
88 return v
89}
90
91func (c *commandDialogCmp) listWidth() int {
92 return defaultWidth - 4 // 4 for padding
93}
94
95func (c *commandDialogCmp) listHeight() int {
96 listHeigh := len(c.commandList.Items()) + 2 // height based on items + 2 for the input
97 return min(listHeigh, c.wHeight/2)
98}
99
100func (c *commandDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
101 offset := 10 + 1
102 cursor.Y += offset
103 _, col := c.Position()
104 cursor.X = cursor.X + col + 2
105 return cursor
106}
107
108func (c *commandDialogCmp) style() lipgloss.Style {
109 t := theme.CurrentTheme()
110 return styles.BaseStyle().
111 Width(c.width).
112 Padding(0, 1, 1, 1).
113 Border(lipgloss.RoundedBorder()).
114 BorderBackground(t.Background()).
115 BorderForeground(t.TextMuted())
116}
117
118func (q *commandDialogCmp) Position() (int, int) {
119 row := 10
120 col := q.wWidth / 2
121 col -= q.width / 2
122 return row, col
123}
124
125func (c *commandDialogCmp) defaultCommands() []Command {
126 return []Command{
127 {
128 ID: "init",
129 Title: "Initialize Project",
130 Description: "Create/Update the OpenCode.md memory file",
131 Handler: func(cmd Command) tea.Cmd {
132 prompt := `Please analyze this codebase and create a OpenCode.md file containing:
133 1. Build/lint/test commands - especially for running a single test
134 2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
135
136 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.
137 If there's already a opencode.md, improve it.
138 If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.`
139 return tea.Batch(
140 util.CmdHandler(chat.SendMsg{
141 Text: prompt,
142 }),
143 )
144 },
145 },
146 {
147 ID: "compact",
148 Title: "Compact Session",
149 Description: "Summarize the current session and create a new one with the summary",
150 Handler: func(cmd Command) tea.Cmd {
151 return func() tea.Msg {
152 // TODO: implement compact message
153 return ""
154 }
155 },
156 },
157 }
158}
159
160func (c *commandDialogCmp) ID() dialogs.DialogID {
161 return id
162}