@@ -1,23 +1,29 @@
1package commands
1package commands
4+ "github.com/charmbracelet/bubbles/v2/help"
4 "github.com/charmbracelet/bubbles/v2/key"
5 "github.com/charmbracelet/bubbles/v2/key"
5 tea "github.com/charmbracelet/bubbletea/v2"
6 tea "github.com/charmbracelet/bubbletea/v2"
6 "github.com/charmbracelet/lipgloss/v2"
7 "github.com/charmbracelet/lipgloss/v2"
8 "github.com/opencode-ai/opencode/internal/tui/components/chat"
9 "github.com/opencode-ai/opencode/internal/tui/components/chat"
9 "github.com/opencode-ai/opencode/internal/tui/components/completions"
10 "github.com/opencode-ai/opencode/internal/tui/components/completions"
11+ "github.com/opencode-ai/opencode/internal/tui/components/core"
10 "github.com/opencode-ai/opencode/internal/tui/components/core/list"
12 "github.com/opencode-ai/opencode/internal/tui/components/core/list"
11 "github.com/opencode-ai/opencode/internal/tui/components/dialogs"
13 "github.com/opencode-ai/opencode/internal/tui/components/dialogs"
12 "github.com/opencode-ai/opencode/internal/tui/styles"
14 "github.com/opencode-ai/opencode/internal/tui/styles"
13− "github.com/opencode-ai/opencode/internal/tui/theme"
14 "github.com/opencode-ai/opencode/internal/tui/util"
15 "github.com/opencode-ai/opencode/internal/tui/util"
18 commandsDialogID dialogs.DialogID = "commands"
19 commandsDialogID dialogs.DialogID = "commands"
20− defaultWidth int = 60
21+ defaultWidth int = 70
25+ SystemCommands int = iota
23// Command represents a command that can be executed
29// Command represents a command that can be executed
@@ -38,11 +44,18 @@ type commandDialogCmp struct {
38 wWidth int // Width of the terminal window
44 wWidth int // Width of the terminal window
39 wHeight int // Height of the terminal window
45 wHeight int // Height of the terminal window
41− commandList list.ListModel
47+ commandList list.ListModel
42− commands []Command
48+ keyMap CommandsDialogKeyMap
43− keyMap CommandsDialogKeyMap
49+ help help.Model
50+ commandType int // SystemCommands or UserCommands
51+ userCommands []Command // User-defined commands
55+ SwitchSessionsMsg struct{}
56+ SwitchModelMsg struct{}
46func NewCommandDialog() CommandsDialog {
59func NewCommandDialog() CommandsDialog {
47 listKeyMap := list.DefaultKeyMap()
60 listKeyMap := list.DefaultKeyMap()
48 keyMap := DefaultCommandsDialogKeyMap()
61 keyMap := DefaultCommandsDialogKeyMap()
@@ -59,11 +72,20 @@ func NewCommandDialog() CommandsDialog {
59 listKeyMap.DownOneItem = keyMap.Next
72 listKeyMap.DownOneItem = keyMap.Next
60 listKeyMap.UpOneItem = keyMap.Previous
73 listKeyMap.UpOneItem = keyMap.Previous
75+ t := styles.CurrentTheme()
76+ commandList := list.New(
77+ list.WithFilterable(true),
62− commandList := list.New(list.WithFilterable(true), list.WithKeyMap(listKeyMap))
78+ list.WithKeyMap(listKeyMap),
79+ list.WithWrapNavigation(true),
82+ help.Styles = t.S().Help
63 return &commandDialogCmp{
83 return &commandDialogCmp{
64 commandList: commandList,
84 commandList: commandList,
65 width: defaultWidth,
85 width: defaultWidth,
66 keyMap: DefaultCommandsDialogKeyMap(),
86 keyMap: DefaultCommandsDialogKeyMap(),
88+ commandType: SystemCommands,
@@ -72,24 +94,9 @@ func (c *commandDialogCmp) Init() tea.Cmd {
72 if err != nil {
94 if err != nil {
73 return util.ReportError(err)
95 return util.ReportError(err)
75− c.commands = commands
77− commandItems := []util.Model{}
78− if len(commands) > 0 {
79− commandItems = append(commandItems, NewItemSection("Custom Commands"))
80− for _, cmd := range commands {
81− commandItems = append(commandItems, completions.NewCompletionItem(cmd.Title, cmd))
85− commandItems = append(commandItems, NewItemSection("Default"))
87− for _, cmd := range c.defaultCommands() {
98+ c.userCommands = commands
88− c.commands = append(c.commands, cmd)
99+ c.SetCommandType(c.commandType)
89− commandItems = append(commandItems, completions.NewCompletionItem(cmd.Title, cmd))
92− c.commandList.SetItems(commandItems)
93 return c.commandList.Init()
100 return c.commandList.Init()
@@ -112,6 +119,13 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
112 util.CmdHandler(dialogs.CloseDialogMsg{}),
119 util.CmdHandler(dialogs.CloseDialogMsg{}),
113 selectedItem.Handler(selectedItem),
120 selectedItem.Handler(selectedItem),
122+ case key.Matches(msg, c.keyMap.Tab):
123+ // Toggle command type between System and User commands
124+ if c.commandType == SystemCommands {
125+ return c, c.SetCommandType(UserCommands)
127+ return c, c.SetCommandType(SystemCommands)
116 u, cmd := c.commandList.Update(msg)
130 u, cmd := c.commandList.Update(msg)
117 c.commandList = u.(list.ListModel)
131 c.commandList = u.(list.ListModel)
@@ -122,8 +136,17 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
124func (c *commandDialogCmp) View() tea.View {
138func (c *commandDialogCmp) View() tea.View {
139+ t := styles.CurrentTheme()
125 listView := c.commandList.View()
140 listView := c.commandList.View()
141+ radio := c.commandTypeRadio()
142+ content := lipgloss.JoinVertical(
144+ t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Commands", c.width-lipgloss.Width(radio)-5)+" "+radio),
147+ t.S().Base.Width(c.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(c.help.View(c.keyMap)),
126− v := tea.NewView(c.style().Render(listView.String()))
149+ v := tea.NewView(c.style().Render(content))
127 if listView.Cursor() != nil {
150 if listView.Cursor() != nil {
128 c := c.moveCursor(listView.Cursor())
151 c := c.moveCursor(listView.Cursor())
129 v.SetCursor(c)
152 v.SetCursor(c)
@@ -131,8 +154,36 @@ func (c *commandDialogCmp) View() tea.View {
157+func (c *commandDialogCmp) commandTypeRadio() string {
158+ t := styles.CurrentTheme()
159+ choices := []string{"System", "User"}
161+ iconUnselected := "○"
162+ if c.commandType == SystemCommands {
163+ return t.S().Text.Render(iconSelected + " " + choices[0] + " " + iconUnselected + " " + choices[1])
165+ return t.S().Text.Render(iconUnselected + " " + choices[0] + " " + iconSelected + " " + choices[1])
134func (c *commandDialogCmp) listWidth() int {
168func (c *commandDialogCmp) listWidth() int {
135− return defaultWidth - 4 // 4 for padding
169+ return defaultWidth - 2 // 4 for padding
172+func (c *commandDialogCmp) SetCommandType(commandType int) tea.Cmd {
173+ c.commandType = commandType
175+ var commands []Command
176+ if c.commandType == SystemCommands {
177+ commands = c.defaultCommands()
179+ commands = c.userCommands
182+ commandItems := []util.Model{}
183+ for _, cmd := range commands {
184+ commandItems = append(commandItems, completions.NewCompletionItem(cmd.Title, cmd))
186+ return c.commandList.SetItems(commandItems)
138func (c *commandDialogCmp) listHeight() int {
189func (c *commandDialogCmp) listHeight() int {
@@ -141,27 +192,25 @@ func (c *commandDialogCmp) listHeight() int {
143func (c *commandDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
194func (c *commandDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
195+ row, col := c.Position()
144− offset := 10 + 1
196+ offset := row + 3
145 cursor.Y += offset
197 cursor.Y += offset
146− _, col := c.Position()
147 cursor.X = cursor.X + col + 2
198 cursor.X = cursor.X + col + 2
148 return cursor
199 return cursor
151func (c *commandDialogCmp) style() lipgloss.Style {
202func (c *commandDialogCmp) style() lipgloss.Style {
152− t := theme.CurrentTheme()
203+ t := styles.CurrentTheme()
153− return styles.BaseStyle().
204+ return t.S().Base.
154 Width(c.width).
205 Width(c.width).
155− Padding(0, 1, 1, 1).
156 Border(lipgloss.RoundedBorder()).
206 Border(lipgloss.RoundedBorder()).
157− BorderBackground(t.Background()).
158− BorderForeground(t.TextMuted())
207+ BorderForeground(t.BorderFocus)
161−func (q *commandDialogCmp) Position() (int, int) {
210+func (c *commandDialogCmp) Position() (int, int) {
162− row := 10
211+ row := c.wHeight/4 - 2 // just a bit above the center
163− col := q.wWidth / 2
212+ col := c.wWidth / 2
164− col -= q.width / 2
213+ col -= c.width / 2
165 return row, col
214 return row, col
@@ -197,6 +246,26 @@ func (c *commandDialogCmp) defaultCommands() []Command {
250+ ID: "switch_session",
251+ Title: "Switch Session",
252+ Description: "Switch to a different session",
253+ Handler: func(cmd Command) tea.Cmd {
254+ return func() tea.Msg {
255+ return SwitchSessionsMsg{}
261+ Title: "Switch Model",
262+ Description: "Switch to a different model",
263+ Handler: func(cmd Command) tea.Cmd {
264+ return func() tea.Msg {
265+ return SwitchModelMsg{}