commands.go

  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}