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	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}