summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/tui/internal/components/dialog/arguments.go264
-rw-r--r--packages/tui/internal/components/dialog/commands.go180
-rw-r--r--packages/tui/internal/components/dialog/custom_commands.go155
-rw-r--r--packages/tui/internal/components/dialog/custom_commands_test.go106
-rw-r--r--packages/tui/internal/components/dialog/tools.go178
-rw-r--r--packages/tui/internal/page/chat.go25
-rw-r--r--packages/tui/internal/tui/tui.go45
7 files changed, 0 insertions, 953 deletions
diff --git a/packages/tui/internal/components/dialog/arguments.go b/packages/tui/internal/components/dialog/arguments.go
deleted file mode 100644
index 4ef21fcf4..000000000
--- a/packages/tui/internal/components/dialog/arguments.go
+++ /dev/null
@@ -1,264 +0,0 @@
-package dialog
-
-import (
- "fmt"
- "github.com/charmbracelet/bubbles/v2/key"
- "github.com/charmbracelet/bubbles/v2/textinput"
- tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/lipgloss/v2"
-
- "github.com/sst/opencode/internal/styles"
- "github.com/sst/opencode/internal/theme"
- "github.com/sst/opencode/internal/util"
-)
-
-type argumentsDialogKeyMap struct {
- Enter key.Binding
- Escape key.Binding
-}
-
-// ShortHelp implements key.Map.
-func (k argumentsDialogKeyMap) ShortHelp() []key.Binding {
- return []key.Binding{
- key.NewBinding(
- key.WithKeys("enter"),
- key.WithHelp("enter", "confirm"),
- ),
- key.NewBinding(
- key.WithKeys("esc"),
- key.WithHelp("esc", "cancel"),
- ),
- }
-}
-
-// FullHelp implements key.Map.
-func (k argumentsDialogKeyMap) FullHelp() [][]key.Binding {
- return [][]key.Binding{k.ShortHelp()}
-}
-
-// ShowMultiArgumentsDialogMsg is a message that is sent to show the multi-arguments dialog.
-type ShowMultiArgumentsDialogMsg struct {
- CommandID string
- Content string
- ArgNames []string
-}
-
-// CloseMultiArgumentsDialogMsg is a message that is sent when the multi-arguments dialog is closed.
-type CloseMultiArgumentsDialogMsg struct {
- Submit bool
- CommandID string
- Content string
- Args map[string]string
-}
-
-// MultiArgumentsDialogCmp is a component that asks the user for multiple command arguments.
-type MultiArgumentsDialogCmp struct {
- width, height int
- inputs []textinput.Model
- focusIndex int
- keys argumentsDialogKeyMap
- commandID string
- content string
- argNames []string
-}
-
-// NewMultiArgumentsDialogCmp creates a new MultiArgumentsDialogCmp.
-func NewMultiArgumentsDialogCmp(commandID, content string, argNames []string) MultiArgumentsDialogCmp {
- t := theme.CurrentTheme()
- inputs := make([]textinput.Model, len(argNames))
-
- for i, name := range argNames {
- ti := textinput.New()
- ti.Placeholder = fmt.Sprintf("Enter value for %s...", name)
- ti.SetWidth(40)
- ti.Prompt = ""
- ti.Styles.Blurred.Placeholder = ti.Styles.Blurred.Placeholder.Background(t.Background())
- ti.Styles.Blurred.Text = ti.Styles.Blurred.Text.Background(t.Background())
- ti.Styles.Blurred.Prompt = ti.Styles.Blurred.Prompt.Foreground(t.Primary())
-
- ti.Styles.Focused.Placeholder = ti.Styles.Focused.Placeholder.Background(t.Background())
- ti.Styles.Focused.Text = ti.Styles.Focused.Text.Background(t.Background())
- ti.Styles.Focused.Prompt = ti.Styles.Focused.Prompt.Foreground(t.Primary())
-
- // ti.PromptStyle = ti.PromptStyle.Background(t.Background())
- // ti.TextStyle = ti.TextStyle.Background(t.Background())
-
- // Only focus the first input initially
- if i == 0 {
- ti.Focus()
- // ti.PromptStyle = ti.PromptStyle.Foreground(t.Primary())
- // ti.TextStyle = ti.TextStyle.Foreground(t.Primary())
- } else {
- ti.Blur()
- }
-
- inputs[i] = ti
- }
-
- return MultiArgumentsDialogCmp{
- inputs: inputs,
- keys: argumentsDialogKeyMap{},
- commandID: commandID,
- content: content,
- argNames: argNames,
- focusIndex: 0,
- }
-}
-
-// Init implements tea.Model.
-func (m MultiArgumentsDialogCmp) Init() tea.Cmd {
- // Make sure only the first input is focused
- for i := range m.inputs {
- if i == 0 {
- m.inputs[i].Focus()
- } else {
- m.inputs[i].Blur()
- }
- }
-
- return textinput.Blink
-}
-
-// Update implements tea.Model.
-func (m MultiArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmds []tea.Cmd
- // t := theme.CurrentTheme()
-
- switch msg := msg.(type) {
- case tea.KeyMsg:
- switch {
- case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
- return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
- Submit: false,
- CommandID: m.commandID,
- Content: m.content,
- Args: nil,
- })
- case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
- // If we're on the last input, submit the form
- if m.focusIndex == len(m.inputs)-1 {
- args := make(map[string]string)
- for i, name := range m.argNames {
- args[name] = m.inputs[i].Value()
- }
- return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
- Submit: true,
- CommandID: m.commandID,
- Content: m.content,
- Args: args,
- })
- }
- // Otherwise, move to the next input
- m.inputs[m.focusIndex].Blur()
- m.focusIndex++
- m.inputs[m.focusIndex].Focus()
- // m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
- // m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
- case key.Matches(msg, key.NewBinding(key.WithKeys("tab"))):
- // Move to the next input
- m.inputs[m.focusIndex].Blur()
- m.focusIndex = (m.focusIndex + 1) % len(m.inputs)
- m.inputs[m.focusIndex].Focus()
- // m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
- // m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
- case key.Matches(msg, key.NewBinding(key.WithKeys("shift+tab"))):
- // Move to the previous input
- m.inputs[m.focusIndex].Blur()
- m.focusIndex = (m.focusIndex - 1 + len(m.inputs)) % len(m.inputs)
- m.inputs[m.focusIndex].Focus()
- // m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
- // m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
- }
- case tea.WindowSizeMsg:
- m.width = msg.Width
- m.height = msg.Height
- }
-
- // Update the focused input
- var cmd tea.Cmd
- m.inputs[m.focusIndex], cmd = m.inputs[m.focusIndex].Update(msg)
- cmds = append(cmds, cmd)
-
- return m, tea.Batch(cmds...)
-}
-
-// View implements tea.Model.
-func (m MultiArgumentsDialogCmp) View() string {
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
-
- // Calculate width needed for content
- maxWidth := 60 // Width for explanation text
-
- title := lipgloss.NewStyle().
- Foreground(t.Primary()).
- Bold(true).
- Width(maxWidth).
- Padding(0, 1).
- Background(t.Background()).
- Render("Command Arguments")
-
- explanation := lipgloss.NewStyle().
- Foreground(t.Text()).
- Width(maxWidth).
- Padding(0, 1).
- Background(t.Background()).
- Render("This command requires multiple arguments. Please enter values for each:")
-
- // Create input fields for each argument
- inputFields := make([]string, len(m.inputs))
- for i, input := range m.inputs {
- // Highlight the label of the focused input
- labelStyle := lipgloss.NewStyle().
- Width(maxWidth).
- Padding(1, 1, 0, 1).
- Background(t.Background())
-
- if i == m.focusIndex {
- labelStyle = labelStyle.Foreground(t.Primary()).Bold(true)
- } else {
- labelStyle = labelStyle.Foreground(t.TextMuted())
- }
-
- label := labelStyle.Render(m.argNames[i] + ":")
-
- field := lipgloss.NewStyle().
- Foreground(t.Text()).
- Width(maxWidth).
- Padding(0, 1).
- Background(t.Background()).
- Render(input.View())
-
- inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
- }
-
- maxWidth = min(maxWidth, m.width-10)
-
- // Join all elements vertically
- elements := []string{title, explanation}
- elements = append(elements, inputFields...)
-
- content := lipgloss.JoinVertical(
- lipgloss.Left,
- elements...,
- )
-
- return baseStyle.Padding(1, 2).
- Border(lipgloss.RoundedBorder()).
- BorderBackground(t.Background()).
- BorderForeground(t.TextMuted()).
- Background(t.Background()).
- Width(lipgloss.Width(content) + 4).
- Render(content)
-}
-
-// SetSize sets the size of the component.
-func (m *MultiArgumentsDialogCmp) SetSize(width, height int) {
- m.width = width
- m.height = height
-}
-
-// Bindings implements layout.Bindings.
-func (m MultiArgumentsDialogCmp) Bindings() []key.Binding {
- return m.keys.ShortHelp()
-}
diff --git a/packages/tui/internal/components/dialog/commands.go b/packages/tui/internal/components/dialog/commands.go
deleted file mode 100644
index 39fe793d7..000000000
--- a/packages/tui/internal/components/dialog/commands.go
+++ /dev/null
@@ -1,180 +0,0 @@
-package dialog
-
-import (
- "github.com/charmbracelet/bubbles/v2/key"
- tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/lipgloss/v2"
- utilComponents "github.com/sst/opencode/internal/components/util"
- "github.com/sst/opencode/internal/layout"
- "github.com/sst/opencode/internal/styles"
- "github.com/sst/opencode/internal/theme"
- "github.com/sst/opencode/internal/util"
-)
-
-// Command represents a command that can be executed
-type Command struct {
- ID string
- Title string
- Description string
- Handler func(cmd Command) tea.Cmd
-}
-
-func (ci Command) Render(selected bool, width int) string {
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
-
- descStyle := baseStyle.Width(width).Foreground(t.TextMuted())
- itemStyle := baseStyle.Width(width).
- Foreground(t.Text()).
- Background(t.Background())
-
- if selected {
- itemStyle = itemStyle.
- Background(t.Primary()).
- Foreground(t.Background()).
- Bold(true)
- descStyle = descStyle.
- Background(t.Primary()).
- Foreground(t.Background())
- }
-
- title := itemStyle.Padding(0, 1).Render(ci.Title)
- if ci.Description != "" {
- description := descStyle.Padding(0, 1).Render(ci.Description)
- return lipgloss.JoinVertical(lipgloss.Left, title, description)
- }
- return title
-}
-
-// CommandSelectedMsg is sent when a command is selected
-type CommandSelectedMsg struct {
- Command Command
-}
-
-// CloseCommandDialogMsg is sent when the command dialog is closed
-type CloseCommandDialogMsg struct{}
-
-// CommandDialog interface for the command selection dialog
-type CommandDialog interface {
- layout.ModelWithView
- layout.Bindings
- SetCommands(commands []Command)
-}
-
-type commandDialogComponent struct {
- listView utilComponents.SimpleList[Command]
- width int
- height int
-}
-
-type commandKeyMap struct {
- Enter key.Binding
- Escape key.Binding
-}
-
-var commandKeys = commandKeyMap{
- Enter: key.NewBinding(
- key.WithKeys("enter"),
- key.WithHelp("enter", "select command"),
- ),
- Escape: key.NewBinding(
- key.WithKeys("esc"),
- key.WithHelp("esc", "close"),
- ),
-}
-
-func (c *commandDialogComponent) Init() tea.Cmd {
- return c.listView.Init()
-}
-
-func (c *commandDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmds []tea.Cmd
- switch msg := msg.(type) {
- case tea.KeyMsg:
- switch {
- case key.Matches(msg, commandKeys.Enter):
- selectedItem, idx := c.listView.GetSelectedItem()
- if idx != -1 {
- return c, util.CmdHandler(CommandSelectedMsg{
- Command: selectedItem,
- })
- }
- case key.Matches(msg, commandKeys.Escape):
- return c, util.CmdHandler(CloseCommandDialogMsg{})
- }
- case tea.WindowSizeMsg:
- c.width = msg.Width
- c.height = msg.Height
- }
-
- u, cmd := c.listView.Update(msg)
- c.listView = u.(utilComponents.SimpleList[Command])
- cmds = append(cmds, cmd)
-
- return c, tea.Batch(cmds...)
-}
-
-func (c *commandDialogComponent) View() string {
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle()
-
- maxWidth := 40
-
- commands := c.listView.GetItems()
-
- for _, cmd := range commands {
- if len(cmd.Title) > maxWidth-4 {
- maxWidth = len(cmd.Title) + 4
- }
- if cmd.Description != "" {
- if len(cmd.Description) > maxWidth-4 {
- maxWidth = len(cmd.Description) + 4
- }
- }
- }
-
- c.listView.SetMaxWidth(maxWidth)
-
- title := baseStyle.
- Foreground(t.Primary()).
- Bold(true).
- Width(maxWidth).
- Padding(0, 1).
- Render("Commands")
-
- content := lipgloss.JoinVertical(
- lipgloss.Left,
- title,
- baseStyle.Width(maxWidth).Render(""),
- baseStyle.Width(maxWidth).Render(c.listView.View()),
- baseStyle.Width(maxWidth).Render(""),
- )
-
- return baseStyle.Padding(1, 2).
- Border(lipgloss.RoundedBorder()).
- BorderBackground(t.Background()).
- BorderForeground(t.TextMuted()).
- Width(lipgloss.Width(content) + 4).
- Render(content)
-}
-
-func (c *commandDialogComponent) BindingKeys() []key.Binding {
- return layout.KeyMapToSlice(commandKeys)
-}
-
-func (c *commandDialogComponent) SetCommands(commands []Command) {
- c.listView.SetItems(commands)
-}
-
-// NewCommandDialogCmp creates a new command selection dialog
-func NewCommandDialogCmp() CommandDialog {
- listView := utilComponents.NewSimpleList[Command](
- []Command{},
- 10,
- "No commands available",
- true,
- )
- return &commandDialogComponent{
- listView: listView,
- }
-}
diff --git a/packages/tui/internal/components/dialog/custom_commands.go b/packages/tui/internal/components/dialog/custom_commands.go
deleted file mode 100644
index 5e0671b80..000000000
--- a/packages/tui/internal/components/dialog/custom_commands.go
+++ /dev/null
@@ -1,155 +0,0 @@
-package dialog
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "regexp"
- "strings"
-
- tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/sst/opencode/internal/app"
- "github.com/sst/opencode/internal/util"
-)
-
-// Command prefix constants
-const (
- UserCommandPrefix = "user:"
- ProjectCommandPrefix = "project:"
-)
-
-// namedArgPattern is a regex pattern to find named arguments in the format $NAME
-var namedArgPattern = regexp.MustCompile(`\$([A-Z][A-Z0-9_]*)`)
-
-// LoadCustomCommands loads custom commands from both XDG_CONFIG_HOME and project data directory
-func LoadCustomCommands() ([]Command, error) {
- var commands []Command
-
- homeCommandsDir := filepath.Join(app.Info.Path.Config, "commands")
- homeCommands, err := loadCommandsFromDir(homeCommandsDir, UserCommandPrefix)
- if err != nil {
- // Log error but continue - we'll still try to load other commands
- fmt.Printf("Warning: failed to load home commands: %v\n", err)
- } else {
- commands = append(commands, homeCommands...)
- }
-
- projectCommandsDir := filepath.Join(app.Info.Path.Root, ".opencode", "commands")
- projectCommands, err := loadCommandsFromDir(projectCommandsDir, ProjectCommandPrefix)
- if err != nil {
- // Log error but return what we have so far
- fmt.Printf("Warning: failed to load project commands: %v\n", err)
- } else {
- commands = append(commands, projectCommands...)
- }
-
- return commands, nil
-}
-
-// loadCommandsFromDir loads commands from a specific directory with the given prefix
-func loadCommandsFromDir(commandsDir string, prefix string) ([]Command, error) {
- // Check if the commands directory exists
- if _, err := os.Stat(commandsDir); os.IsNotExist(err) {
- // Create the commands directory if it doesn't exist
- if err := os.MkdirAll(commandsDir, 0755); err != nil {
- return nil, fmt.Errorf("failed to create commands directory %s: %w", commandsDir, err)
- }
- // Return empty list since we just created the directory
- return []Command{}, nil
- }
-
- var commands []Command
-
- // Walk through the commands directory and load all .md files
- err := filepath.Walk(commandsDir, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return err
- }
-
- // Skip directories
- if info.IsDir() {
- return nil
- }
-
- // Only process markdown files
- if !strings.HasSuffix(strings.ToLower(info.Name()), ".md") {
- return nil
- }
-
- // Read the file content
- content, err := os.ReadFile(path)
- if err != nil {
- return fmt.Errorf("failed to read command file %s: %w", path, err)
- }
-
- // Get the command ID from the file name without the .md extension
- commandID := strings.TrimSuffix(info.Name(), filepath.Ext(info.Name()))
-
- // Get relative path from commands directory
- relPath, err := filepath.Rel(commandsDir, path)
- if err != nil {
- return fmt.Errorf("failed to get relative path for %s: %w", path, err)
- }
-
- // Create the command ID from the relative path
- // Replace directory separators with colons
- commandIDPath := strings.ReplaceAll(filepath.Dir(relPath), string(filepath.Separator), ":")
- if commandIDPath != "." {
- commandID = commandIDPath + ":" + commandID
- }
-
- // Create a command
- command := Command{
- ID: prefix + commandID,
- Title: prefix + commandID,
- Description: fmt.Sprintf("Custom command from %s", relPath),
- Handler: func(cmd Command) tea.Cmd {
- commandContent := string(content)
-
- // Check for named arguments
- matches := namedArgPattern.FindAllStringSubmatch(commandContent, -1)
- if len(matches) > 0 {
- // Extract unique argument names
- argNames := make([]string, 0)
- argMap := make(map[string]bool)
-
- for _, match := range matches {
- argName := match[1] // Group 1 is the name without $
- if !argMap[argName] {
- argMap[argName] = true
- argNames = append(argNames, argName)
- }
- }
-
- // Show multi-arguments dialog for all named arguments
- return util.CmdHandler(ShowMultiArgumentsDialogMsg{
- CommandID: cmd.ID,
- Content: commandContent,
- ArgNames: argNames,
- })
- }
-
- // No arguments needed, run command directly
- return util.CmdHandler(CommandRunCustomMsg{
- Content: commandContent,
- Args: nil, // No arguments
- })
- },
- }
-
- commands = append(commands, command)
- return nil
- })
-
- if err != nil {
- return nil, fmt.Errorf("failed to load custom commands from %s: %w", commandsDir, err)
- }
-
- return commands, nil
-}
-
-// CommandRunCustomMsg is sent when a custom command is executed
-type CommandRunCustomMsg struct {
- Content string
- Args map[string]string // Map of argument names to values
-}
diff --git a/packages/tui/internal/components/dialog/custom_commands_test.go b/packages/tui/internal/components/dialog/custom_commands_test.go
deleted file mode 100644
index 3468ac3b0..000000000
--- a/packages/tui/internal/components/dialog/custom_commands_test.go
+++ /dev/null
@@ -1,106 +0,0 @@
-package dialog
-
-import (
- "testing"
- "regexp"
-)
-
-func TestNamedArgPattern(t *testing.T) {
- testCases := []struct {
- input string
- expected []string
- }{
- {
- input: "This is a test with $ARGUMENTS placeholder",
- expected: []string{"ARGUMENTS"},
- },
- {
- input: "This is a test with $FOO and $BAR placeholders",
- expected: []string{"FOO", "BAR"},
- },
- {
- input: "This is a test with $FOO_BAR and $BAZ123 placeholders",
- expected: []string{"FOO_BAR", "BAZ123"},
- },
- {
- input: "This is a test with no placeholders",
- expected: []string{},
- },
- {
- input: "This is a test with $FOO appearing twice: $FOO",
- expected: []string{"FOO"},
- },
- {
- input: "This is a test with $1INVALID placeholder",
- expected: []string{},
- },
- }
-
- for _, tc := range testCases {
- matches := namedArgPattern.FindAllStringSubmatch(tc.input, -1)
-
- // Extract unique argument names
- argNames := make([]string, 0)
- argMap := make(map[string]bool)
-
- for _, match := range matches {
- argName := match[1] // Group 1 is the name without $
- if !argMap[argName] {
- argMap[argName] = true
- argNames = append(argNames, argName)
- }
- }
-
- // Check if we got the expected number of arguments
- if len(argNames) != len(tc.expected) {
- t.Errorf("Expected %d arguments, got %d for input: %s", len(tc.expected), len(argNames), tc.input)
- continue
- }
-
- // Check if we got the expected argument names
- for _, expectedArg := range tc.expected {
- found := false
- for _, actualArg := range argNames {
- if actualArg == expectedArg {
- found = true
- break
- }
- }
- if !found {
- t.Errorf("Expected argument %s not found in %v for input: %s", expectedArg, argNames, tc.input)
- }
- }
- }
-}
-
-func TestRegexPattern(t *testing.T) {
- pattern := regexp.MustCompile(`\$([A-Z][A-Z0-9_]*)`)
-
- validMatches := []string{
- "$FOO",
- "$BAR",
- "$FOO_BAR",
- "$BAZ123",
- "$ARGUMENTS",
- }
-
- invalidMatches := []string{
- "$foo",
- "$1BAR",
- "$_FOO",
- "FOO",
- "$",
- }
-
- for _, valid := range validMatches {
- if !pattern.MatchString(valid) {
- t.Errorf("Expected %s to match, but it didn't", valid)
- }
- }
-
- for _, invalid := range invalidMatches {
- if pattern.MatchString(invalid) {
- t.Errorf("Expected %s not to match, but it did", invalid)
- }
- }
-} \ No newline at end of file
diff --git a/packages/tui/internal/components/dialog/tools.go b/packages/tui/internal/components/dialog/tools.go
deleted file mode 100644
index c91dfef9a..000000000
--- a/packages/tui/internal/components/dialog/tools.go
+++ /dev/null
@@ -1,178 +0,0 @@
-package dialog
-
-import (
- "github.com/charmbracelet/bubbles/v2/key"
- tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/lipgloss/v2"
- utilComponents "github.com/sst/opencode/internal/components/util"
- "github.com/sst/opencode/internal/layout"
- "github.com/sst/opencode/internal/styles"
- "github.com/sst/opencode/internal/theme"
-)
-
-const (
- maxToolsDialogWidth = 60
- maxVisibleTools = 15
-)
-
-// ToolsDialog interface for the tools list dialog
-type ToolsDialog interface {
- layout.ModelWithView
- layout.Bindings
- SetTools(tools []string)
-}
-
-// ShowToolsDialogMsg is sent to show the tools dialog
-type ShowToolsDialogMsg struct {
- Show bool
-}
-
-// CloseToolsDialogMsg is sent when the tools dialog is closed
-type CloseToolsDialogMsg struct{}
-
-type toolItem struct {
- name string
-}
-
-func (t toolItem) Render(selected bool, width int) string {
- th := theme.CurrentTheme()
- baseStyle := styles.BaseStyle().
- Width(width).
- Background(th.Background())
-
- if selected {
- baseStyle = baseStyle.
- Background(th.Primary()).
- Foreground(th.Background()).
- Bold(true)
- } else {
- baseStyle = baseStyle.
- Foreground(th.Text())
- }
-
- return baseStyle.Render(t.name)
-}
-
-type toolsDialogComponent struct {
- tools []toolItem
- width int
- height int
- list utilComponents.SimpleList[toolItem]
-}
-
-type toolsKeyMap struct {
- Up key.Binding
- Down key.Binding
- Escape key.Binding
- J key.Binding
- K key.Binding
-}
-
-var toolsKeys = toolsKeyMap{
- Up: key.NewBinding(
- key.WithKeys("up"),
- key.WithHelp("↑", "previous tool"),
- ),
- Down: key.NewBinding(
- key.WithKeys("down"),
- key.WithHelp("↓", "next tool"),
- ),
- Escape: key.NewBinding(
- key.WithKeys("esc"),
- key.WithHelp("esc", "close"),
- ),
- J: key.NewBinding(
- key.WithKeys("j"),
- key.WithHelp("j", "next tool"),
- ),
- K: key.NewBinding(
- key.WithKeys("k"),
- key.WithHelp("k", "previous tool"),
- ),
-}
-
-func (m *toolsDialogComponent) Init() tea.Cmd {
- return nil
-}
-
-func (m *toolsDialogComponent) SetTools(tools []string) {
- var toolItems []toolItem
- for _, name := range tools {
- toolItems = append(toolItems, toolItem{name: name})
- }
-
- m.tools = toolItems
- m.list.SetItems(toolItems)
-}
-
-func (m *toolsDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- switch msg := msg.(type) {
- case tea.KeyMsg:
- switch {
- case key.Matches(msg, toolsKeys.Escape):
- return m, func() tea.Msg { return CloseToolsDialogMsg{} }
- // Pass other key messages to the list component
- default:
- var cmd tea.Cmd
- listModel, cmd := m.list.Update(msg)
- m.list = listModel.(utilComponents.SimpleList[toolItem])
- return m, cmd
- }
- case tea.WindowSizeMsg:
- m.width = msg.Width
- m.height = msg.Height
- }
-
- // For non-key messages
- var cmd tea.Cmd
- listModel, cmd := m.list.Update(msg)
- m.list = listModel.(utilComponents.SimpleList[toolItem])
- return m, cmd
-}
-
-func (m *toolsDialogComponent) View() string {
- t := theme.CurrentTheme()
- baseStyle := styles.BaseStyle().Background(t.Background())
-
- title := baseStyle.
- Foreground(t.Primary()).
- Bold(true).
- Width(maxToolsDialogWidth).
- Padding(0, 0, 1).
- Render("Available Tools")
-
- // Calculate dialog width based on content
- dialogWidth := min(maxToolsDialogWidth, m.width/2)
- m.list.SetMaxWidth(dialogWidth)
-
- content := lipgloss.JoinVertical(
- lipgloss.Left,
- title,
- m.list.View(),
- )
-
- return baseStyle.Padding(1, 2).
- Border(lipgloss.RoundedBorder()).
- BorderBackground(t.Background()).
- BorderForeground(t.TextMuted()).
- Background(t.Background()).
- Width(lipgloss.Width(content) + 4).
- Render(content)
-}
-
-func (m *toolsDialogComponent) BindingKeys() []key.Binding {
- return layout.KeyMapToSlice(toolsKeys)
-}
-
-func NewToolsDialogCmp() ToolsDialog {
- list := utilComponents.NewSimpleList[toolItem](
- []toolItem{},
- maxVisibleTools,
- "No tools available",
- true,
- )
-
- return &toolsDialogComponent{
- list: list,
- }
-}
diff --git a/packages/tui/internal/page/chat.go b/packages/tui/internal/page/chat.go
index 525d77d97..3e2e530a0 100644
--- a/packages/tui/internal/page/chat.go
+++ b/packages/tui/internal/page/chat.go
@@ -2,7 +2,6 @@ package page
import (
"context"
- "strings"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
@@ -12,7 +11,6 @@ import (
"github.com/sst/opencode/internal/components/chat"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/layout"
- "github.com/sst/opencode/internal/status"
"github.com/sst/opencode/internal/util"
)
@@ -67,29 +65,6 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if cmd != nil {
return p, cmd
}
- case dialog.CommandRunCustomMsg:
- // Check if the agent is busy before executing custom commands
- if p.app.IsBusy() {
- status.Warn("Agent is busy, please wait before executing a command...")
- return p, nil
- }
-
- // Process the command content with arguments if any
- content := msg.Content
- if msg.Args != nil {
- // Replace all named arguments with their values
- for name, value := range msg.Args {
- placeholder := "$" + name
- content = strings.ReplaceAll(content, placeholder, value)
- }
- }
-
- // Handle custom command execution
- cmd := p.sendMessage(content, nil)
- if cmd != nil {
- return p, cmd
- }
-
case dialog.CompletionDialogCloseMsg:
p.showCompletionDialog = false
p.app.SetCompletionDialogOpen(false)
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index 03fc2e0dd..6de884078 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -2,7 +2,6 @@ package tui
import (
"context"
- "log/slog"
"github.com/charmbracelet/bubbles/v2/cursor"
"github.com/charmbracelet/bubbles/v2/key"
@@ -17,7 +16,6 @@ import (
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/page"
"github.com/sst/opencode/internal/state"
- "github.com/sst/opencode/internal/status"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
@@ -69,7 +67,6 @@ type appModel struct {
status core.StatusComponent
app *app.App
modal layout.Modal
- commands []dialog.Command
}
func (a appModel) Init() tea.Cmd {
@@ -348,11 +345,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, tea.Batch(cmds...)
}
-// RegisterCommand adds a command to the command dialog
-func (a *appModel) RegisterCommand(cmd dialog.Command) {
- a.commands = append(a.commands, cmd)
-}
-
func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
var cmds []tea.Cmd
if _, ok := a.loadedPages[pageID]; !ok {
@@ -391,47 +383,10 @@ func NewModel(app *app.App) tea.Model {
loadedPages: make(map[page.PageID]bool),
status: core.NewStatusCmp(app),
app: app,
- commands: []dialog.Command{},
pages: map[page.PageID]layout.ModelWithView{
page.ChatPage: page.NewChatPage(app),
},
}
- model.RegisterCommand(dialog.Command{
- ID: "init",
- Title: "Initialize Project",
- Description: "Create/Update the AGENTS.md memory file",
- Handler: func(cmd dialog.Command) tea.Cmd {
- return app.InitializeProject(context.Background())
- },
- })
-
- model.RegisterCommand(dialog.Command{
- ID: "compact_conversation",
- Title: "Compact Conversation",
- Description: "Summarize the current session to save tokens",
- Handler: func(cmd dialog.Command) tea.Cmd {
- // Get the current session from the appModel
- if model.currentPage != page.ChatPage {
- status.Warn("Please navigate to a chat session first.")
- return nil
- }
-
- // Return a message that will be handled by the chat page
- status.Info("Compacting conversation...")
- return util.CmdHandler(state.CompactSessionMsg{})
- },
- })
-
- // Load custom commands
- customCommands, err := dialog.LoadCustomCommands()
- if err != nil {
- slog.Warn("Failed to load custom commands", "error", err)
- } else {
- for _, cmd := range customCommands {
- model.RegisterCommand(cmd)
- }
- }
-
return model
}