mirror of
https://github.com/sst/opencode.git
synced 2025-08-04 13:30:52 +00:00
wip: refactoring tui
This commit is contained in:
parent
e8e03c895a
commit
5706c6ad3a
4 changed files with 147 additions and 82 deletions
25
packages/tui/internal/commands/command.go
Normal file
25
packages/tui/internal/commands/command.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
)
|
||||
|
||||
// Command represents a user-triggerable action.
|
||||
type Command struct {
|
||||
// Name is the identifier used for slash commands (e.g., "new").
|
||||
Name string
|
||||
// Description is a short explanation of what the command does.
|
||||
Description string
|
||||
// KeyBinding is the keyboard shortcut to trigger this command.
|
||||
KeyBinding key.Binding
|
||||
}
|
||||
|
||||
// Registry holds all the available commands.
|
||||
type Registry struct {
|
||||
Commands map[string]Command
|
||||
}
|
||||
|
||||
// ExecuteCommandMsg is a message sent when a command should be executed.
|
||||
type ExecuteCommandMsg struct {
|
||||
Name string
|
||||
}
|
|
@ -13,6 +13,7 @@ import (
|
|||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/commands"
|
||||
"github.com/sst/opencode/internal/components/dialog"
|
||||
"github.com/sst/opencode/internal/image"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
|
@ -365,7 +366,7 @@ func (m *editorComponent) openEditor(value string) tea.Cmd {
|
|||
}
|
||||
|
||||
func (m *editorComponent) send() tea.Cmd {
|
||||
value := m.textarea.Value()
|
||||
value := strings.TrimSpace(m.textarea.Value())
|
||||
m.textarea.Reset()
|
||||
attachments := m.attachments
|
||||
|
||||
|
@ -382,6 +383,13 @@ func (m *editorComponent) send() tea.Cmd {
|
|||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for slash command
|
||||
if strings.HasPrefix(value, "/") {
|
||||
commandName := strings.TrimPrefix(value, "/")
|
||||
return util.CmdHandler(commands.ExecuteCommandMsg{Name: commandName})
|
||||
}
|
||||
|
||||
return tea.Batch(
|
||||
util.CmdHandler(SendMsg{
|
||||
Text: value,
|
||||
|
|
|
@ -230,7 +230,7 @@ func renderText(message client.MessageInfo, text string, author string) string {
|
|||
case client.Assistant:
|
||||
return renderContentBlock(content,
|
||||
WithAlign(lipgloss.Left),
|
||||
WithBorderColor(t.Primary()),
|
||||
WithBorderColor(t.Accent()),
|
||||
)
|
||||
}
|
||||
return ""
|
||||
|
@ -250,8 +250,12 @@ func renderToolInvocation(
|
|||
outerWidth := layout.Current.Container.Width
|
||||
innerWidth := outerWidth - 6
|
||||
paddingTop := 0
|
||||
paddingBottom := 0
|
||||
if showResult {
|
||||
paddingTop = 1
|
||||
if result == nil || *result == "" {
|
||||
paddingBottom = 1
|
||||
}
|
||||
}
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
|
@ -259,6 +263,7 @@ func renderToolInvocation(
|
|||
Width(outerWidth).
|
||||
Background(t.BackgroundSubtle()).
|
||||
PaddingTop(paddingTop).
|
||||
PaddingBottom(paddingBottom).
|
||||
PaddingLeft(2).
|
||||
PaddingRight(2).
|
||||
BorderLeft(true).
|
||||
|
@ -301,10 +306,17 @@ func renderToolInvocation(
|
|||
if e, ok := metadata.Get("error"); ok && e.(bool) == true {
|
||||
if m, ok := metadata.Get("message"); ok {
|
||||
body = "" // don't show the body if there's an error
|
||||
style = style.BorderLeftForeground(t.Error())
|
||||
error = styles.BaseStyle().
|
||||
Background(t.BackgroundSubtle()).
|
||||
Foreground(t.Error()).
|
||||
Render(m.(string))
|
||||
error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithMarginBottom(1))
|
||||
error = renderContentBlock(
|
||||
error,
|
||||
WithFullWidth(),
|
||||
WithBorderColor(t.Error()),
|
||||
WithMarginBottom(1),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/charmbracelet/lipgloss/v2"
|
||||
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/commands"
|
||||
"github.com/sst/opencode/internal/components/core"
|
||||
"github.com/sst/opencode/internal/components/dialog"
|
||||
"github.com/sst/opencode/internal/components/modal"
|
||||
|
@ -22,41 +23,7 @@ import (
|
|||
"github.com/sst/opencode/pkg/client"
|
||||
)
|
||||
|
||||
type keyMap struct {
|
||||
Help key.Binding
|
||||
NewSession key.Binding
|
||||
SwitchSession key.Binding
|
||||
SwitchModel key.Binding
|
||||
SwitchTheme key.Binding
|
||||
Quit key.Binding
|
||||
}
|
||||
|
||||
var keys = keyMap{
|
||||
Help: key.NewBinding(
|
||||
key.WithKeys("f1", "super+/", "super+h"),
|
||||
key.WithHelp("/help", "show help"),
|
||||
),
|
||||
NewSession: key.NewBinding(
|
||||
key.WithKeys("f2", "super+n"),
|
||||
key.WithHelp("/new", "new session"),
|
||||
),
|
||||
SwitchSession: key.NewBinding(
|
||||
key.WithKeys("f3", "super+s"),
|
||||
key.WithHelp("/sessions", "switch session"),
|
||||
),
|
||||
SwitchModel: key.NewBinding(
|
||||
key.WithKeys("f4", "super+m"),
|
||||
key.WithHelp("/model", "switch model"),
|
||||
),
|
||||
SwitchTheme: key.NewBinding(
|
||||
key.WithKeys("f5", "super+t"),
|
||||
key.WithHelp("/theme", "switch theme"),
|
||||
),
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("f10", "ctrl+c", "super+q"),
|
||||
key.WithHelp("/quit", "quit"),
|
||||
),
|
||||
}
|
||||
|
||||
type appModel struct {
|
||||
width, height int
|
||||
|
@ -67,6 +34,7 @@ type appModel struct {
|
|||
status core.StatusComponent
|
||||
app *app.App
|
||||
modal layout.Modal
|
||||
commands *commands.Registry
|
||||
}
|
||||
|
||||
func (a appModel) Init() tea.Cmd {
|
||||
|
@ -131,12 +99,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
}
|
||||
|
||||
isModalTrigger = key.Matches(msg, keys.NewSession) ||
|
||||
key.Matches(msg, keys.SwitchSession) ||
|
||||
key.Matches(msg, keys.SwitchModel) ||
|
||||
key.Matches(msg, keys.SwitchTheme) ||
|
||||
key.Matches(msg, keys.Help) ||
|
||||
key.Matches(msg, keys.Quit)
|
||||
for _, cmdDef := range a.commands.Commands {
|
||||
if key.Matches(msg, cmdDef.KeyBinding) {
|
||||
isModalTrigger = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !isModalTrigger {
|
||||
|
@ -148,6 +116,38 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case commands.ExecuteCommandMsg:
|
||||
switch msg.Name {
|
||||
case "quit":
|
||||
quitDialog := dialog.NewQuitDialog()
|
||||
a.modal = quitDialog
|
||||
case "new":
|
||||
a.app.Session = &client.SessionInfo{}
|
||||
a.app.Messages = []client.MessageInfo{}
|
||||
cmds = append(cmds, util.CmdHandler(state.SessionClearedMsg{}))
|
||||
case "sessions":
|
||||
sessionDialog := dialog.NewSessionDialog(a.app)
|
||||
a.modal = sessionDialog
|
||||
case "model":
|
||||
modelDialog := dialog.NewModelDialog(a.app)
|
||||
a.modal = modelDialog
|
||||
case "theme":
|
||||
themeDialog := dialog.NewThemeDialog()
|
||||
a.modal = themeDialog
|
||||
case "help":
|
||||
var helpBindings []key.Binding
|
||||
for _, cmd := range a.commands.Commands {
|
||||
// Create a new binding for help display
|
||||
helpBindings = append(helpBindings, key.NewBinding(
|
||||
key.WithKeys(cmd.KeyBinding.Keys()...),
|
||||
key.WithHelp("/"+cmd.Name, cmd.Description),
|
||||
))
|
||||
}
|
||||
helpDialog := dialog.NewHelpDialog(helpBindings...)
|
||||
a.modal = helpDialog
|
||||
}
|
||||
return a, tea.Batch(cmds...)
|
||||
|
||||
case tea.BackgroundColorMsg:
|
||||
styles.Terminal = &styles.TerminalInfo{
|
||||
BackgroundIsDark: msg.IsDark(),
|
||||
|
@ -276,45 +276,15 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case key.Matches(msg, keys.Help):
|
||||
helpDialog := dialog.NewHelpDialog(
|
||||
keys.Help,
|
||||
keys.NewSession,
|
||||
keys.SwitchSession,
|
||||
keys.SwitchModel,
|
||||
keys.SwitchTheme,
|
||||
keys.Quit,
|
||||
)
|
||||
a.modal = helpDialog
|
||||
return a, nil
|
||||
|
||||
case key.Matches(msg, keys.NewSession):
|
||||
a.app.Session = &client.SessionInfo{}
|
||||
a.app.Messages = []client.MessageInfo{}
|
||||
return a, tea.Batch(
|
||||
util.CmdHandler(state.SessionClearedMsg{}),
|
||||
)
|
||||
|
||||
case key.Matches(msg, keys.SwitchModel):
|
||||
modelDialog := dialog.NewModelDialog(a.app)
|
||||
a.modal = modelDialog
|
||||
return a, nil
|
||||
|
||||
case key.Matches(msg, keys.SwitchSession):
|
||||
sessionDialog := dialog.NewSessionDialog(a.app)
|
||||
a.modal = sessionDialog
|
||||
return a, nil
|
||||
|
||||
case key.Matches(msg, keys.SwitchTheme):
|
||||
themeDialog := dialog.NewThemeDialog()
|
||||
a.modal = themeDialog
|
||||
return a, nil
|
||||
|
||||
case key.Matches(msg, keys.Quit):
|
||||
quitDialog := dialog.NewQuitDialog()
|
||||
a.modal = quitDialog
|
||||
return a, nil
|
||||
// First, check for modal triggers from the command registry
|
||||
if a.modal == nil {
|
||||
for _, cmdDef := range a.commands.Commands {
|
||||
if key.Matches(msg, cmdDef.KeyBinding) {
|
||||
// If a key matches, send an ExecuteCommandMsg to self.
|
||||
// This unifies keybinding and slash command handling.
|
||||
return a, util.CmdHandler(commands.ExecuteCommandMsg{Name: cmdDef.Name})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -361,6 +331,55 @@ func (a appModel) View() string {
|
|||
return appView
|
||||
}
|
||||
|
||||
func newCommandRegistry() *commands.Registry {
|
||||
return &commands.Registry{
|
||||
Commands: map[string]commands.Command{
|
||||
"help": {
|
||||
Name: "help",
|
||||
Description: "show help",
|
||||
KeyBinding: key.NewBinding(
|
||||
key.WithKeys("f1", "super+/", "super+h"),
|
||||
),
|
||||
},
|
||||
"new": {
|
||||
Name: "new",
|
||||
Description: "new session",
|
||||
KeyBinding: key.NewBinding(
|
||||
key.WithKeys("f2", "super+n"),
|
||||
),
|
||||
},
|
||||
"sessions": {
|
||||
Name: "sessions",
|
||||
Description: "switch session",
|
||||
KeyBinding: key.NewBinding(
|
||||
key.WithKeys("f3", "super+s"),
|
||||
),
|
||||
},
|
||||
"model": {
|
||||
Name: "model",
|
||||
Description: "switch model",
|
||||
KeyBinding: key.NewBinding(
|
||||
key.WithKeys("f4", "super+m"),
|
||||
),
|
||||
},
|
||||
"theme": {
|
||||
Name: "theme",
|
||||
Description: "switch theme",
|
||||
KeyBinding: key.NewBinding(
|
||||
key.WithKeys("f5", "super+t"),
|
||||
),
|
||||
},
|
||||
"quit": {
|
||||
Name: "quit",
|
||||
Description: "quit",
|
||||
KeyBinding: key.NewBinding(
|
||||
key.WithKeys("f10", "ctrl+c", "super+q"),
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewModel(app *app.App) tea.Model {
|
||||
startPage := page.ChatPage
|
||||
model := &appModel{
|
||||
|
@ -368,6 +387,7 @@ func NewModel(app *app.App) tea.Model {
|
|||
loadedPages: make(map[page.PageID]bool),
|
||||
status: core.NewStatusCmp(app),
|
||||
app: app,
|
||||
commands: newCommandRegistry(),
|
||||
pages: map[page.PageID]layout.ModelWithView{
|
||||
page.ChatPage: page.NewChatPage(app),
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue