adamdottv 8 месяцев назад
Родитель
Сommit
3728a12bee

+ 2 - 2
packages/tui/internal/app/app.go

@@ -23,7 +23,7 @@ type App struct {
 	Info      client.AppInfo
 	Info      client.AppInfo
 	Version   string
 	Version   string
 	StatePath string
 	StatePath string
-	Configg   *client.ConfigInfo
+	Config    *client.ConfigInfo
 	Client    *client.ClientWithResponses
 	Client    *client.ClientWithResponses
 	State     *config.State
 	State     *config.State
 	Provider  *client.ProviderInfo
 	Provider  *client.ProviderInfo
@@ -95,7 +95,7 @@ func New(
 		Info:      appInfo,
 		Info:      appInfo,
 		Version:   version,
 		Version:   version,
 		StatePath: appStatePath,
 		StatePath: appStatePath,
-		Configg:   configInfo,
+		Config:    configInfo,
 		State:     appState,
 		State:     appState,
 		Client:    httpClient,
 		Client:    httpClient,
 		Session:   &client.SessionInfo{},
 		Session:   &client.SessionInfo{},

+ 1 - 1
packages/tui/internal/commands/command.go

@@ -186,7 +186,7 @@ func LoadFromConfig(config *client.ConfigInfo) CommandRegistry {
 		},
 		},
 		{
 		{
 			Name:        ProjectInitCommand,
 			Name:        ProjectInitCommand,
-			Description: "create or update AGENTS.md",
+			Description: "create/update AGENTS.md",
 			Keybindings: parseBindings("<leader>i"),
 			Keybindings: parseBindings("<leader>i"),
 			Trigger:     "init",
 			Trigger:     "init",
 		},
 		},

+ 45 - 31
packages/tui/internal/components/chat/messages.go

@@ -10,6 +10,7 @@ import (
 	tea "github.com/charmbracelet/bubbletea/v2"
 	tea "github.com/charmbracelet/bubbletea/v2"
 	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/sst/opencode/internal/app"
 	"github.com/sst/opencode/internal/app"
+	"github.com/sst/opencode/internal/components/commands"
 	"github.com/sst/opencode/internal/components/dialog"
 	"github.com/sst/opencode/internal/components/dialog"
 	"github.com/sst/opencode/internal/layout"
 	"github.com/sst/opencode/internal/layout"
 	"github.com/sst/opencode/internal/styles"
 	"github.com/sst/opencode/internal/styles"
@@ -37,6 +38,7 @@ type messagesComponent struct {
 	viewport        viewport.Model
 	viewport        viewport.Model
 	spinner         spinner.Model
 	spinner         spinner.Model
 	attachments     viewport.Model
 	attachments     viewport.Model
+	commands        commands.CommandsComponent
 	cache           *MessageCache
 	cache           *MessageCache
 	rendering       bool
 	rendering       bool
 	showToolDetails bool
 	showToolDetails bool
@@ -46,7 +48,7 @@ type renderFinishedMsg struct{}
 type ToggleToolDetailsMsg struct{}
 type ToggleToolDetailsMsg struct{}
 
 
 func (m *messagesComponent) Init() tea.Cmd {
 func (m *messagesComponent) Init() tea.Cmd {
-	return tea.Batch(m.viewport.Init(), m.spinner.Tick)
+	return tea.Batch(m.viewport.Init(), m.spinner.Tick, m.commands.Init())
 }
 }
 
 
 func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -93,6 +95,11 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	spinner, cmd := m.spinner.Update(msg)
 	spinner, cmd := m.spinner.Update(msg)
 	m.spinner = spinner
 	m.spinner = spinner
 	cmds = append(cmds, cmd)
 	cmds = append(cmds, cmd)
+
+	updated, cmd := m.commands.Update(msg)
+	m.commands = updated.(commands.CommandsComponent)
+	cmds = append(cmds, cmd)
+
 	return m, tea.Batch(cmds...)
 	return m, tea.Batch(cmds...)
 }
 }
 
 
@@ -281,7 +288,13 @@ func (m *messagesComponent) View() string {
 		return m.home()
 		return m.home()
 	}
 	}
 	if m.rendering {
 	if m.rendering {
-		return m.viewport.View()
+		return lipgloss.Place(
+			m.width,
+			m.height,
+			lipgloss.Center,
+			lipgloss.Center,
+			"Loading session...",
+		)
 	}
 	}
 	t := theme.CurrentTheme()
 	t := theme.CurrentTheme()
 	return lipgloss.JoinVertical(
 	return lipgloss.JoinVertical(
@@ -319,50 +332,42 @@ func (m *messagesComponent) home() string {
 	// cwd := app.Info.Path.Cwd
 	// cwd := app.Info.Path.Cwd
 	// config := app.Info.Path.Config
 	// config := app.Info.Path.Config
 
 
-	commands := [][]string{
-		{"/help", "show help"},
-		{"/sessions", "list sessions"},
-		{"/new", "start a new session"},
-		{"/model", "switch model"},
-		{"/theme", "switch theme"},
-		{"/exit", "exit the app"},
-	}
-
-	commandLines := []string{}
-	for _, command := range commands {
-		commandLines = append(commandLines, (base(command[0]+" ") + muted(command[1])))
-	}
+	versionStyle := lipgloss.NewStyle().
+		Background(t.Background()).
+		Foreground(t.TextMuted()).
+		Width(lipgloss.Width(logo)).
+		Align(lipgloss.Right)
+	version := versionStyle.Render(m.app.Version)
 
 
-	logoAndVersion := lipgloss.JoinVertical(
-		lipgloss.Right,
-		logo,
-		muted(m.app.Version),
+	logoAndVersion := strings.Join([]string{logo, version}, "\n")
+	logoAndVersion = lipgloss.PlaceHorizontal(
+		m.width,
+		lipgloss.Center,
+		logoAndVersion,
+		lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
+	)
+	commands := lipgloss.PlaceHorizontal(
+		m.width,
+		lipgloss.Center,
+		m.commands.View(),
+		lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
 	)
 	)
 
 
 	lines := []string{}
 	lines := []string{}
-	lines = append(lines, "")
-	lines = append(lines, "")
 	lines = append(lines, logoAndVersion)
 	lines = append(lines, logoAndVersion)
 	lines = append(lines, "")
 	lines = append(lines, "")
+	lines = append(lines, "")
 	// lines = append(lines, base("cwd ")+muted(cwd))
 	// lines = append(lines, base("cwd ")+muted(cwd))
 	// lines = append(lines, base("config ")+muted(config))
 	// lines = append(lines, base("config ")+muted(config))
 	// lines = append(lines, "")
 	// lines = append(lines, "")
-	lines = append(lines, commandLines...)
-	lines = append(lines, "")
-	if m.rendering {
-		lines = append(lines, base("Loading session..."))
-	} else {
-		lines = append(lines, "")
-	}
+	lines = append(lines, commands)
 
 
 	return lipgloss.Place(
 	return lipgloss.Place(
 		m.width,
 		m.width,
 		m.height,
 		m.height,
 		lipgloss.Center,
 		lipgloss.Center,
 		lipgloss.Center,
 		lipgloss.Center,
-		baseStyle.Width(lipgloss.Width(logoAndVersion)).Render(
-			strings.Join(lines, "\n"),
-		),
+		baseStyle.Render(strings.Join(lines, "\n")),
 		lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
 		lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
 	)
 	)
 }
 }
@@ -381,6 +386,7 @@ func (m *messagesComponent) SetSize(width, height int) tea.Cmd {
 	m.viewport.SetHeight(height - lipgloss.Height(m.header()))
 	m.viewport.SetHeight(height - lipgloss.Height(m.header()))
 	m.attachments.SetWidth(width + 40)
 	m.attachments.SetWidth(width + 40)
 	m.attachments.SetHeight(3)
 	m.attachments.SetHeight(3)
+	m.commands.SetSize(width, height)
 	m.renderView()
 	m.renderView()
 	return nil
 	return nil
 }
 }
@@ -444,11 +450,19 @@ func NewMessagesComponent(app *app.App) MessagesComponent {
 	attachments := viewport.New()
 	attachments := viewport.New()
 	vp.KeyMap = viewport.KeyMap{}
 	vp.KeyMap = viewport.KeyMap{}
 
 
+	t := theme.CurrentTheme()
+	commandsView := commands.New(
+		app,
+		commands.WithBackground(t.Background()),
+		commands.WithLimit(6),
+	)
+
 	return &messagesComponent{
 	return &messagesComponent{
 		app:             app,
 		app:             app,
 		viewport:        vp,
 		viewport:        vp,
 		spinner:         s,
 		spinner:         s,
 		attachments:     attachments,
 		attachments:     attachments,
+		commands:        commandsView,
 		showToolDetails: true,
 		showToolDetails: true,
 		cache:           NewMessageCache(),
 		cache:           NewMessageCache(),
 		tail:            true,
 		tail:            true,

+ 196 - 0
packages/tui/internal/components/commands/commands.go

@@ -0,0 +1,196 @@
+package commands
+
+import (
+	"fmt"
+	"strings"
+
+	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/lipgloss/v2"
+	"github.com/charmbracelet/lipgloss/v2/compat"
+	"github.com/sst/opencode/internal/app"
+	"github.com/sst/opencode/internal/commands"
+	"github.com/sst/opencode/internal/layout"
+	"github.com/sst/opencode/internal/styles"
+	"github.com/sst/opencode/internal/theme"
+)
+
+type CommandsComponent interface {
+	tea.Model
+	tea.ViewModel
+	layout.Sizeable
+}
+
+type commandsComponent struct {
+	app           *app.App
+	width, height int
+	showKeybinds  bool
+	background    *compat.AdaptiveColor
+	limit         *int
+}
+
+func (c *commandsComponent) SetSize(width, height int) tea.Cmd {
+	c.width = width
+	c.height = height
+	return nil
+}
+
+func (c *commandsComponent) GetSize() (int, int) {
+	return c.width, c.height
+}
+
+func (c *commandsComponent) Init() tea.Cmd {
+	return nil
+}
+
+func (c *commandsComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	switch msg := msg.(type) {
+	case tea.WindowSizeMsg:
+		c.width = msg.Width
+		c.height = msg.Height
+	}
+	return c, nil
+}
+
+func (c *commandsComponent) View() string {
+	t := theme.CurrentTheme()
+
+	triggerStyle := lipgloss.NewStyle().
+		Foreground(t.Primary()).
+		Bold(true)
+
+	descriptionStyle := lipgloss.NewStyle().
+		Foreground(t.Text())
+
+	keybindStyle := lipgloss.NewStyle().
+		Foreground(t.TextMuted())
+
+	if c.background != nil {
+		triggerStyle = triggerStyle.Background(*c.background)
+		descriptionStyle = descriptionStyle.Background(*c.background)
+		keybindStyle = keybindStyle.Background(*c.background)
+	}
+
+	var commandsWithTriggers []commands.Command
+	for _, cmd := range c.app.Commands.Sorted() {
+		if cmd.Trigger != "" {
+			commandsWithTriggers = append(commandsWithTriggers, cmd)
+		}
+	}
+	if c.limit != nil && len(commandsWithTriggers) > *c.limit {
+		commandsWithTriggers = commandsWithTriggers[:*c.limit]
+	}
+
+	if len(commandsWithTriggers) == 0 {
+		return styles.Muted().Render("No commands with triggers available")
+	}
+
+	// Calculate column widths
+	maxTriggerWidth := 0
+	maxDescriptionWidth := 0
+	maxKeybindWidth := 0
+
+	// Prepare command data
+	type commandRow struct {
+		trigger     string
+		description string
+		keybinds    string
+	}
+
+	rows := make([]commandRow, 0, len(commandsWithTriggers))
+
+	for _, cmd := range commandsWithTriggers {
+		trigger := "/" + cmd.Trigger
+		description := cmd.Description
+
+		// Format keybindings
+		var keybindStrs []string
+		if c.showKeybinds {
+			for _, kb := range cmd.Keybindings {
+				if kb.RequiresLeader {
+					keybindStrs = append(keybindStrs, *c.app.Config.Keybinds.Leader+" "+kb.Key)
+				} else {
+					keybindStrs = append(keybindStrs, kb.Key)
+				}
+			}
+		}
+		keybinds := strings.Join(keybindStrs, ", ")
+
+		rows = append(rows, commandRow{
+			trigger:     trigger,
+			description: description,
+			keybinds:    keybinds,
+		})
+
+		// Update max widths
+		if len(trigger) > maxTriggerWidth {
+			maxTriggerWidth = len(trigger)
+		}
+		if len(description) > maxDescriptionWidth {
+			maxDescriptionWidth = len(description)
+		}
+		if len(keybinds) > maxKeybindWidth {
+			maxKeybindWidth = len(keybinds)
+		}
+	}
+
+	// Add padding between columns
+	columnPadding := 3
+
+	// Build the output
+	var output strings.Builder
+
+	for _, row := range rows {
+		// Pad each column to align properly
+		trigger := fmt.Sprintf("%-*s", maxTriggerWidth, row.trigger)
+		description := fmt.Sprintf("%-*s", maxDescriptionWidth, row.description)
+
+		// Apply styles and combine
+		line := triggerStyle.Render(trigger) +
+			triggerStyle.Render(strings.Repeat(" ", columnPadding)) +
+			descriptionStyle.Render(description)
+
+		if c.showKeybinds && row.keybinds != "" {
+			line += keybindStyle.Render(strings.Repeat(" ", columnPadding)) +
+				keybindStyle.Render(row.keybinds)
+		}
+
+		output.WriteString(line + "\n")
+	}
+
+	// Remove trailing newline
+	result := strings.TrimSuffix(output.String(), "\n")
+
+	return result
+}
+
+type Option func(*commandsComponent)
+
+func WithKeybinds(show bool) Option {
+	return func(c *commandsComponent) {
+		c.showKeybinds = show
+	}
+}
+
+func WithBackground(background compat.AdaptiveColor) Option {
+	return func(c *commandsComponent) {
+		c.background = &background
+	}
+}
+
+func WithLimit(limit int) Option {
+	return func(c *commandsComponent) {
+		c.limit = &limit
+	}
+}
+
+func New(app *app.App, opts ...Option) CommandsComponent {
+	c := &commandsComponent{
+		app:          app,
+		background:   nil,
+		showKeybinds: true,
+	}
+	for _, opt := range opts {
+		opt(c)
+	}
+	return c
+}

+ 2 - 2
packages/tui/internal/tui/tui.go

@@ -556,8 +556,8 @@ func NewModel(app *app.App) tea.Model {
 	messagesContainer := layout.NewContainer(messages)
 	messagesContainer := layout.NewContainer(messages)
 
 
 	var leaderBinding *key.Binding
 	var leaderBinding *key.Binding
-	if (*app.Configg.Keybinds).Leader != nil {
-		binding := key.NewBinding(key.WithKeys(*app.Configg.Keybinds.Leader))
+	if (*app.Config.Keybinds).Leader != nil {
+		binding := key.NewBinding(key.WithKeys(*app.Config.Keybinds.Leader))
 		leaderBinding = &binding
 		leaderBinding = &binding
 	}
 	}