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 commandsDialogID 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 commandItems := []util.Model{}
58 if len(commands) > 0 {
59 commandItems = append(commandItems, NewItemSection("Custom"))
60 for _, cmd := range commands {
61 commandItems = append(commandItems, NewCommandItem(cmd))
62 }
63 }
64
65 commandItems = append(commandItems, NewItemSection("Default"))
66
67 for _, cmd := range c.defaultCommands() {
68 commandItems = append(commandItems, NewCommandItem(cmd))
69 }
70
71 c.commandList.SetItems(commandItems)
72 return c.commandList.Init()
73}
74
75func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
76 switch msg := msg.(type) {
77 case tea.WindowSizeMsg:
78 c.wWidth = msg.Width
79 c.wHeight = msg.Height
80 return c, c.commandList.SetSize(c.listWidth(), c.listHeight())
81 }
82 u, cmd := c.commandList.Update(msg)
83 c.commandList = u.(list.ListModel)
84 return c, cmd
85}
86
87func (c *commandDialogCmp) View() tea.View {
88 listView := c.commandList.View()
89 v := tea.NewView(c.style().Render(listView.String()))
90 if listView.Cursor() != nil {
91 c := c.moveCursor(listView.Cursor())
92 v.SetCursor(c)
93 }
94 return v
95}
96
97func (c *commandDialogCmp) listWidth() int {
98 return defaultWidth - 4 // 4 for padding
99}
100
101func (c *commandDialogCmp) listHeight() int {
102 listHeigh := len(c.commandList.Items()) + 2 + 4 // height based on items + 2 for the input + 4 for the sections
103 return min(listHeigh, c.wHeight/2)
104}
105
106func (c *commandDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
107 offset := 10 + 1
108 cursor.Y += offset
109 _, col := c.Position()
110 cursor.X = cursor.X + col + 2
111 return cursor
112}
113
114func (c *commandDialogCmp) style() lipgloss.Style {
115 t := theme.CurrentTheme()
116 return styles.BaseStyle().
117 Width(c.width).
118 Padding(0, 1, 1, 1).
119 Border(lipgloss.RoundedBorder()).
120 BorderBackground(t.Background()).
121 BorderForeground(t.TextMuted())
122}
123
124func (q *commandDialogCmp) Position() (int, int) {
125 row := 10
126 col := q.wWidth / 2
127 col -= q.width / 2
128 return row, col
129}
130
131func (c *commandDialogCmp) defaultCommands() []Command {
132 return []Command{
133 {
134 ID: "init",
135 Title: "Initialize Project",
136 Description: "Create/Update the OpenCode.md memory file",
137 Handler: func(cmd Command) tea.Cmd {
138 prompt := `Please analyze this codebase and create a OpenCode.md file containing:
139 1. Build/lint/test commands - especially for running a single test
140 2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
141
142 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.
143 If there's already a opencode.md, improve it.
144 If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.`
145 return tea.Batch(
146 util.CmdHandler(chat.SendMsg{
147 Text: prompt,
148 }),
149 )
150 },
151 },
152 {
153 ID: "compact",
154 Title: "Compact Session",
155 Description: "Summarize the current session and create a new one with the summary",
156 Handler: func(cmd Command) tea.Cmd {
157 return func() tea.Msg {
158 // TODO: implement compact message
159 return ""
160 }
161 },
162 },
163 }
164}
165
166func (c *commandDialogCmp) ID() dialogs.DialogID {
167 return commandsDialogID
168}