summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-06-19 15:56:20 -0500
committeradamdottv <[email protected]>2025-06-19 15:56:28 -0500
commit3728a12bee441f559710b3813787d5f4dce7f5ef (patch)
treefab57cc89828d5a682a6474bd0aba9a14d7a6a76
parentaf07e5121352ebb3a162b6fc16f29c41b928dc89 (diff)
downloadopencode-3728a12bee441f559710b3813787d5f4dce7f5ef.tar.gz
opencode-3728a12bee441f559710b3813787d5f4dce7f5ef.zip
fix(tui): better help on home
-rw-r--r--packages/tui/internal/app/app.go4
-rw-r--r--packages/tui/internal/commands/command.go2
-rw-r--r--packages/tui/internal/components/chat/messages.go76
-rw-r--r--packages/tui/internal/components/commands/commands.go196
-rw-r--r--packages/tui/internal/tui/tui.go4
5 files changed, 246 insertions, 36 deletions
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go
index 72e6ad5eb..787a12844 100644
--- a/packages/tui/internal/app/app.go
+++ b/packages/tui/internal/app/app.go
@@ -23,7 +23,7 @@ type App struct {
Info client.AppInfo
Version string
StatePath string
- Configg *client.ConfigInfo
+ Config *client.ConfigInfo
Client *client.ClientWithResponses
State *config.State
Provider *client.ProviderInfo
@@ -95,7 +95,7 @@ func New(
Info: appInfo,
Version: version,
StatePath: appStatePath,
- Configg: configInfo,
+ Config: configInfo,
State: appState,
Client: httpClient,
Session: &client.SessionInfo{},
diff --git a/packages/tui/internal/commands/command.go b/packages/tui/internal/commands/command.go
index bfa11121b..fc27025f6 100644
--- a/packages/tui/internal/commands/command.go
+++ b/packages/tui/internal/commands/command.go
@@ -186,7 +186,7 @@ func LoadFromConfig(config *client.ConfigInfo) CommandRegistry {
},
{
Name: ProjectInitCommand,
- Description: "create or update AGENTS.md",
+ Description: "create/update AGENTS.md",
Keybindings: parseBindings("<leader>i"),
Trigger: "init",
},
diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go
index cee8aea8f..6130ce090 100644
--- a/packages/tui/internal/components/chat/messages.go
+++ b/packages/tui/internal/components/chat/messages.go
@@ -10,6 +10,7 @@ import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"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/layout"
"github.com/sst/opencode/internal/styles"
@@ -37,6 +38,7 @@ type messagesComponent struct {
viewport viewport.Model
spinner spinner.Model
attachments viewport.Model
+ commands commands.CommandsComponent
cache *MessageCache
rendering bool
showToolDetails bool
@@ -46,7 +48,7 @@ type renderFinishedMsg struct{}
type ToggleToolDetailsMsg struct{}
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) {
@@ -93,6 +95,11 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
spinner, cmd := m.spinner.Update(msg)
m.spinner = spinner
cmds = append(cmds, cmd)
+
+ updated, cmd := m.commands.Update(msg)
+ m.commands = updated.(commands.CommandsComponent)
+ cmds = append(cmds, cmd)
+
return m, tea.Batch(cmds...)
}
@@ -281,7 +288,13 @@ func (m *messagesComponent) View() string {
return m.home()
}
if m.rendering {
- return m.viewport.View()
+ return lipgloss.Place(
+ m.width,
+ m.height,
+ lipgloss.Center,
+ lipgloss.Center,
+ "Loading session...",
+ )
}
t := theme.CurrentTheme()
return lipgloss.JoinVertical(
@@ -319,50 +332,42 @@ func (m *messagesComponent) home() string {
// cwd := app.Info.Path.Cwd
// 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 = append(lines, "")
- lines = append(lines, "")
lines = append(lines, logoAndVersion)
lines = append(lines, "")
+ lines = append(lines, "")
// lines = append(lines, base("cwd ")+muted(cwd))
// lines = append(lines, base("config ")+muted(config))
// 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(
m.width,
m.height,
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())),
)
}
@@ -381,6 +386,7 @@ func (m *messagesComponent) SetSize(width, height int) tea.Cmd {
m.viewport.SetHeight(height - lipgloss.Height(m.header()))
m.attachments.SetWidth(width + 40)
m.attachments.SetHeight(3)
+ m.commands.SetSize(width, height)
m.renderView()
return nil
}
@@ -444,11 +450,19 @@ func NewMessagesComponent(app *app.App) MessagesComponent {
attachments := viewport.New()
vp.KeyMap = viewport.KeyMap{}
+ t := theme.CurrentTheme()
+ commandsView := commands.New(
+ app,
+ commands.WithBackground(t.Background()),
+ commands.WithLimit(6),
+ )
+
return &messagesComponent{
app: app,
viewport: vp,
spinner: s,
attachments: attachments,
+ commands: commandsView,
showToolDetails: true,
cache: NewMessageCache(),
tail: true,
diff --git a/packages/tui/internal/components/commands/commands.go b/packages/tui/internal/components/commands/commands.go
new file mode 100644
index 000000000..75b27779b
--- /dev/null
+++ b/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
+}
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index 0ce22a8ee..c18f00d63 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -556,8 +556,8 @@ func NewModel(app *app.App) tea.Model {
messagesContainer := layout.NewContainer(messages)
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
}