mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-27 04:34:22 +01:00
init command
This commit is contained in:
247
internal/tui/components/dialog/commands.go
Normal file
247
internal/tui/components/dialog/commands.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/layout"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
// Command represents a command that can be executed
|
||||
type Command struct {
|
||||
ID string
|
||||
Title string
|
||||
Description string
|
||||
Handler func(cmd Command) tea.Cmd
|
||||
}
|
||||
|
||||
// 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 {
|
||||
tea.Model
|
||||
layout.Bindings
|
||||
SetCommands(commands []Command)
|
||||
SetSelectedCommand(commandID string)
|
||||
}
|
||||
|
||||
type commandDialogCmp struct {
|
||||
commands []Command
|
||||
selectedIdx int
|
||||
width int
|
||||
height int
|
||||
selectedCommandID string
|
||||
}
|
||||
|
||||
type commandKeyMap struct {
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Enter key.Binding
|
||||
Escape key.Binding
|
||||
J key.Binding
|
||||
K key.Binding
|
||||
}
|
||||
|
||||
var commandKeys = commandKeyMap{
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up"),
|
||||
key.WithHelp("↑", "previous command"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down"),
|
||||
key.WithHelp("↓", "next command"),
|
||||
),
|
||||
Enter: key.NewBinding(
|
||||
key.WithKeys("enter"),
|
||||
key.WithHelp("enter", "select command"),
|
||||
),
|
||||
Escape: key.NewBinding(
|
||||
key.WithKeys("esc"),
|
||||
key.WithHelp("esc", "close"),
|
||||
),
|
||||
J: key.NewBinding(
|
||||
key.WithKeys("j"),
|
||||
key.WithHelp("j", "next command"),
|
||||
),
|
||||
K: key.NewBinding(
|
||||
key.WithKeys("k"),
|
||||
key.WithHelp("k", "previous command"),
|
||||
),
|
||||
}
|
||||
|
||||
func (c *commandDialogCmp) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, commandKeys.Up) || key.Matches(msg, commandKeys.K):
|
||||
if c.selectedIdx > 0 {
|
||||
c.selectedIdx--
|
||||
}
|
||||
return c, nil
|
||||
case key.Matches(msg, commandKeys.Down) || key.Matches(msg, commandKeys.J):
|
||||
if c.selectedIdx < len(c.commands)-1 {
|
||||
c.selectedIdx++
|
||||
}
|
||||
return c, nil
|
||||
case key.Matches(msg, commandKeys.Enter):
|
||||
if len(c.commands) > 0 {
|
||||
return c, util.CmdHandler(CommandSelectedMsg{
|
||||
Command: c.commands[c.selectedIdx],
|
||||
})
|
||||
}
|
||||
case key.Matches(msg, commandKeys.Escape):
|
||||
return c, util.CmdHandler(CloseCommandDialogMsg{})
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
c.width = msg.Width
|
||||
c.height = msg.Height
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *commandDialogCmp) View() string {
|
||||
if len(c.commands) == 0 {
|
||||
return styles.BaseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(styles.Background).
|
||||
BorderForeground(styles.ForgroundDim).
|
||||
Width(40).
|
||||
Render("No commands available")
|
||||
}
|
||||
|
||||
// Calculate max width needed for command titles
|
||||
maxWidth := 40 // Minimum width
|
||||
for _, cmd := range c.commands {
|
||||
if len(cmd.Title) > maxWidth-4 { // Account for padding
|
||||
maxWidth = len(cmd.Title) + 4
|
||||
}
|
||||
if len(cmd.Description) > maxWidth-4 {
|
||||
maxWidth = len(cmd.Description) + 4
|
||||
}
|
||||
}
|
||||
|
||||
// Limit height to avoid taking up too much screen space
|
||||
maxVisibleCommands := min(10, len(c.commands))
|
||||
|
||||
// Build the command list
|
||||
commandItems := make([]string, 0, maxVisibleCommands)
|
||||
startIdx := 0
|
||||
|
||||
// If we have more commands than can be displayed, adjust the start index
|
||||
if len(c.commands) > maxVisibleCommands {
|
||||
// Center the selected item when possible
|
||||
halfVisible := maxVisibleCommands / 2
|
||||
if c.selectedIdx >= halfVisible && c.selectedIdx < len(c.commands)-halfVisible {
|
||||
startIdx = c.selectedIdx - halfVisible
|
||||
} else if c.selectedIdx >= len(c.commands)-halfVisible {
|
||||
startIdx = len(c.commands) - maxVisibleCommands
|
||||
}
|
||||
}
|
||||
|
||||
endIdx := min(startIdx+maxVisibleCommands, len(c.commands))
|
||||
|
||||
for i := startIdx; i < endIdx; i++ {
|
||||
cmd := c.commands[i]
|
||||
itemStyle := styles.BaseStyle.Width(maxWidth)
|
||||
descStyle := styles.BaseStyle.Width(maxWidth).Foreground(styles.ForgroundDim)
|
||||
|
||||
if i == c.selectedIdx {
|
||||
itemStyle = itemStyle.
|
||||
Background(styles.PrimaryColor).
|
||||
Foreground(styles.Background).
|
||||
Bold(true)
|
||||
descStyle = descStyle.
|
||||
Background(styles.PrimaryColor).
|
||||
Foreground(styles.Background)
|
||||
}
|
||||
|
||||
title := itemStyle.Padding(0, 1).Render(cmd.Title)
|
||||
description := ""
|
||||
if cmd.Description != "" {
|
||||
description = descStyle.Padding(0, 1).Render(cmd.Description)
|
||||
commandItems = append(commandItems, lipgloss.JoinVertical(lipgloss.Left, title, description))
|
||||
} else {
|
||||
commandItems = append(commandItems, title)
|
||||
}
|
||||
}
|
||||
|
||||
title := styles.BaseStyle.
|
||||
Foreground(styles.PrimaryColor).
|
||||
Bold(true).
|
||||
Width(maxWidth).
|
||||
Padding(0, 1).
|
||||
Render("Commands")
|
||||
|
||||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
title,
|
||||
styles.BaseStyle.Width(maxWidth).Render(""),
|
||||
styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)),
|
||||
styles.BaseStyle.Width(maxWidth).Render(""),
|
||||
styles.BaseStyle.Width(maxWidth).Padding(0, 1).Foreground(styles.ForgroundDim).Render("↑/k: up ↓/j: down enter: select esc: cancel"),
|
||||
)
|
||||
|
||||
return styles.BaseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(styles.Background).
|
||||
BorderForeground(styles.ForgroundDim).
|
||||
Width(lipgloss.Width(content) + 4).
|
||||
Render(content)
|
||||
}
|
||||
|
||||
func (c *commandDialogCmp) BindingKeys() []key.Binding {
|
||||
return layout.KeyMapToSlice(commandKeys)
|
||||
}
|
||||
|
||||
func (c *commandDialogCmp) SetCommands(commands []Command) {
|
||||
c.commands = commands
|
||||
|
||||
// If we have a selected command ID, find its index
|
||||
if c.selectedCommandID != "" {
|
||||
for i, cmd := range commands {
|
||||
if cmd.ID == c.selectedCommandID {
|
||||
c.selectedIdx = i
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to first command if selected not found
|
||||
c.selectedIdx = 0
|
||||
}
|
||||
|
||||
func (c *commandDialogCmp) SetSelectedCommand(commandID string) {
|
||||
c.selectedCommandID = commandID
|
||||
|
||||
// Update the selected index if commands are already loaded
|
||||
if len(c.commands) > 0 {
|
||||
for i, cmd := range c.commands {
|
||||
if cmd.ID == commandID {
|
||||
c.selectedIdx = i
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewCommandDialogCmp creates a new command selection dialog
|
||||
func NewCommandDialogCmp() CommandDialog {
|
||||
return &commandDialogCmp{
|
||||
commands: []Command{},
|
||||
selectedIdx: 0,
|
||||
selectedCommandID: "",
|
||||
}
|
||||
}
|
||||
|
||||
191
internal/tui/components/dialog/init.go
Normal file
191
internal/tui/components/dialog/init.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
// InitDialogCmp is a component that asks the user if they want to initialize the project.
|
||||
type InitDialogCmp struct {
|
||||
width, height int
|
||||
selected int
|
||||
keys initDialogKeyMap
|
||||
}
|
||||
|
||||
// NewInitDialogCmp creates a new InitDialogCmp.
|
||||
func NewInitDialogCmp() InitDialogCmp {
|
||||
return InitDialogCmp{
|
||||
selected: 0,
|
||||
keys: initDialogKeyMap{},
|
||||
}
|
||||
}
|
||||
|
||||
type initDialogKeyMap struct {
|
||||
Tab key.Binding
|
||||
Left key.Binding
|
||||
Right key.Binding
|
||||
Enter key.Binding
|
||||
Escape key.Binding
|
||||
Y key.Binding
|
||||
N key.Binding
|
||||
}
|
||||
|
||||
// ShortHelp implements key.Map.
|
||||
func (k initDialogKeyMap) ShortHelp() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(
|
||||
key.WithKeys("tab", "left", "right"),
|
||||
key.WithHelp("tab/←/→", "toggle selection"),
|
||||
),
|
||||
key.NewBinding(
|
||||
key.WithKeys("enter"),
|
||||
key.WithHelp("enter", "confirm"),
|
||||
),
|
||||
key.NewBinding(
|
||||
key.WithKeys("esc"),
|
||||
key.WithHelp("esc", "cancel"),
|
||||
),
|
||||
key.NewBinding(
|
||||
key.WithKeys("y", "n"),
|
||||
key.WithHelp("y/n", "yes/no"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// FullHelp implements key.Map.
|
||||
func (k initDialogKeyMap) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{k.ShortHelp()}
|
||||
}
|
||||
|
||||
// Init implements tea.Model.
|
||||
func (m InitDialogCmp) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update implements tea.Model.
|
||||
func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
|
||||
return m, util.CmdHandler(CloseInitDialogMsg{Initialize: false})
|
||||
case key.Matches(msg, key.NewBinding(key.WithKeys("tab", "left", "right", "h", "l"))):
|
||||
m.selected = (m.selected + 1) % 2
|
||||
return m, nil
|
||||
case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
|
||||
return m, util.CmdHandler(CloseInitDialogMsg{Initialize: m.selected == 0})
|
||||
case key.Matches(msg, key.NewBinding(key.WithKeys("y"))):
|
||||
return m, util.CmdHandler(CloseInitDialogMsg{Initialize: true})
|
||||
case key.Matches(msg, key.NewBinding(key.WithKeys("n"))):
|
||||
return m, util.CmdHandler(CloseInitDialogMsg{Initialize: false})
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// View implements tea.Model.
|
||||
func (m InitDialogCmp) View() string {
|
||||
// Calculate width needed for content
|
||||
maxWidth := 60 // Width for explanation text
|
||||
|
||||
title := styles.BaseStyle.
|
||||
Foreground(styles.PrimaryColor).
|
||||
Bold(true).
|
||||
Width(maxWidth).
|
||||
Padding(0, 1).
|
||||
Render("Initialize Project")
|
||||
|
||||
explanation := styles.BaseStyle.
|
||||
Foreground(styles.Forground).
|
||||
Width(maxWidth).
|
||||
Padding(0, 1).
|
||||
Render("Initialization generates a new OpenCode.md file that contains information about your codebase, this file serves as memory for each project, you can freely add to it to help the agents be better at their job.")
|
||||
|
||||
question := styles.BaseStyle.
|
||||
Foreground(styles.Forground).
|
||||
Width(maxWidth).
|
||||
Padding(1, 1).
|
||||
Render("Would you like to initialize this project?")
|
||||
|
||||
yesStyle := styles.BaseStyle
|
||||
noStyle := styles.BaseStyle
|
||||
|
||||
if m.selected == 0 {
|
||||
yesStyle = yesStyle.
|
||||
Background(styles.PrimaryColor).
|
||||
Foreground(styles.Background).
|
||||
Bold(true)
|
||||
noStyle = noStyle.
|
||||
Background(styles.Background).
|
||||
Foreground(styles.PrimaryColor)
|
||||
} else {
|
||||
noStyle = noStyle.
|
||||
Background(styles.PrimaryColor).
|
||||
Foreground(styles.Background).
|
||||
Bold(true)
|
||||
yesStyle = yesStyle.
|
||||
Background(styles.Background).
|
||||
Foreground(styles.PrimaryColor)
|
||||
}
|
||||
|
||||
yes := yesStyle.Padding(0, 3).Render("Yes")
|
||||
no := noStyle.Padding(0, 3).Render("No")
|
||||
|
||||
buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, styles.BaseStyle.Render(" "), no)
|
||||
buttons = styles.BaseStyle.
|
||||
Width(maxWidth).
|
||||
Padding(1, 0).
|
||||
Render(buttons)
|
||||
|
||||
help := styles.BaseStyle.
|
||||
Width(maxWidth).
|
||||
Padding(0, 1).
|
||||
Foreground(styles.ForgroundDim).
|
||||
Render("tab/←/→: toggle y/n: yes/no enter: confirm esc: cancel")
|
||||
|
||||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
title,
|
||||
styles.BaseStyle.Width(maxWidth).Render(""),
|
||||
explanation,
|
||||
question,
|
||||
buttons,
|
||||
styles.BaseStyle.Width(maxWidth).Render(""),
|
||||
help,
|
||||
)
|
||||
|
||||
return styles.BaseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(styles.Background).
|
||||
BorderForeground(styles.ForgroundDim).
|
||||
Width(lipgloss.Width(content) + 4).
|
||||
Render(content)
|
||||
}
|
||||
|
||||
// SetSize sets the size of the component.
|
||||
func (m *InitDialogCmp) SetSize(width, height int) {
|
||||
m.width = width
|
||||
m.height = height
|
||||
}
|
||||
|
||||
// Bindings implements layout.Bindings.
|
||||
func (m InitDialogCmp) Bindings() []key.Binding {
|
||||
return m.keys.ShortHelp()
|
||||
}
|
||||
|
||||
// CloseInitDialogMsg is a message that is sent when the init dialog is closed.
|
||||
type CloseInitDialogMsg struct {
|
||||
Initialize bool
|
||||
}
|
||||
|
||||
// ShowInitDialogMsg is a message that is sent to show the init dialog.
|
||||
type ShowInitDialogMsg struct {
|
||||
Show bool
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/opencode/internal/app"
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/kujtimiihoxha/opencode/internal/permission"
|
||||
"github.com/kujtimiihoxha/opencode/internal/pubsub"
|
||||
@@ -23,6 +24,7 @@ type keyMap struct {
|
||||
Quit key.Binding
|
||||
Help key.Binding
|
||||
SwitchSession key.Binding
|
||||
Commands key.Binding
|
||||
}
|
||||
|
||||
var keys = keyMap{
|
||||
@@ -44,6 +46,11 @@ var keys = keyMap{
|
||||
key.WithKeys("ctrl+a"),
|
||||
key.WithHelp("ctrl+a", "switch session"),
|
||||
),
|
||||
|
||||
Commands: key.NewBinding(
|
||||
key.WithKeys("ctrl+k"),
|
||||
key.WithHelp("ctrl+K", "commands"),
|
||||
),
|
||||
}
|
||||
|
||||
var helpEsc = key.NewBinding(
|
||||
@@ -82,6 +89,13 @@ type appModel struct {
|
||||
showSessionDialog bool
|
||||
sessionDialog dialog.SessionDialog
|
||||
|
||||
showCommandDialog bool
|
||||
commandDialog dialog.CommandDialog
|
||||
commands []dialog.Command
|
||||
|
||||
showInitDialog bool
|
||||
initDialog dialog.InitDialogCmp
|
||||
|
||||
editingMode bool
|
||||
}
|
||||
|
||||
@@ -98,6 +112,23 @@ func (a appModel) Init() tea.Cmd {
|
||||
cmds = append(cmds, cmd)
|
||||
cmd = a.sessionDialog.Init()
|
||||
cmds = append(cmds, cmd)
|
||||
cmd = a.commandDialog.Init()
|
||||
cmds = append(cmds, cmd)
|
||||
cmd = a.initDialog.Init()
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
// Check if we should show the init dialog
|
||||
cmds = append(cmds, func() tea.Msg {
|
||||
shouldShow, err := config.ShouldShowInitDialog()
|
||||
if err != nil {
|
||||
return util.InfoMsg{
|
||||
Type: util.InfoTypeError,
|
||||
Msg: "Failed to check init status: " + err.Error(),
|
||||
}
|
||||
}
|
||||
return dialog.ShowInitDialogMsg{Show: shouldShow}
|
||||
})
|
||||
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
@@ -126,6 +157,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
a.sessionDialog = session.(dialog.SessionDialog)
|
||||
cmds = append(cmds, sessionCmd)
|
||||
|
||||
command, commandCmd := a.commandDialog.Update(msg)
|
||||
a.commandDialog = command.(dialog.CommandDialog)
|
||||
cmds = append(cmds, commandCmd)
|
||||
|
||||
a.initDialog.SetSize(msg.Width, msg.Height)
|
||||
|
||||
return a, tea.Batch(cmds...)
|
||||
case chat.EditorFocusMsg:
|
||||
a.editingMode = bool(msg)
|
||||
@@ -207,6 +244,35 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
a.showSessionDialog = false
|
||||
return a, nil
|
||||
|
||||
case dialog.CloseCommandDialogMsg:
|
||||
a.showCommandDialog = false
|
||||
return a, nil
|
||||
|
||||
case dialog.ShowInitDialogMsg:
|
||||
a.showInitDialog = msg.Show
|
||||
return a, nil
|
||||
|
||||
case dialog.CloseInitDialogMsg:
|
||||
a.showInitDialog = false
|
||||
if msg.Initialize {
|
||||
// Run the initialization command
|
||||
for _, cmd := range a.commands {
|
||||
if cmd.ID == "init" {
|
||||
// Mark the project as initialized
|
||||
if err := config.MarkProjectInitialized(); err != nil {
|
||||
return a, util.ReportError(err)
|
||||
}
|
||||
return a, cmd.Handler(cmd)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Mark the project as initialized without running the command
|
||||
if err := config.MarkProjectInitialized(); err != nil {
|
||||
return a, util.ReportError(err)
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
|
||||
case chat.SessionSelectedMsg:
|
||||
a.sessionDialog.SetSelectedSession(msg.ID)
|
||||
case dialog.SessionSelectedMsg:
|
||||
@@ -216,6 +282,14 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return a, nil
|
||||
|
||||
case dialog.CommandSelectedMsg:
|
||||
a.showCommandDialog = false
|
||||
// Execute the command handler if available
|
||||
if msg.Command.Handler != nil {
|
||||
return a, msg.Command.Handler(msg.Command)
|
||||
}
|
||||
return a, util.ReportInfo("Command selected: " + msg.Command.Title)
|
||||
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, keys.Quit):
|
||||
@@ -226,9 +300,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if a.showSessionDialog {
|
||||
a.showSessionDialog = false
|
||||
}
|
||||
if a.showCommandDialog {
|
||||
a.showCommandDialog = false
|
||||
}
|
||||
return a, nil
|
||||
case key.Matches(msg, keys.SwitchSession):
|
||||
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions {
|
||||
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showCommandDialog {
|
||||
// Load sessions and show the dialog
|
||||
sessions, err := a.app.Sessions.List(context.Background())
|
||||
if err != nil {
|
||||
@@ -242,6 +319,17 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return a, nil
|
||||
}
|
||||
return a, nil
|
||||
case key.Matches(msg, keys.Commands):
|
||||
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog {
|
||||
// Show commands dialog
|
||||
if len(a.commands) == 0 {
|
||||
return a, util.ReportWarn("No commands available")
|
||||
}
|
||||
a.commandDialog.SetCommands(a.commands)
|
||||
a.showCommandDialog = true
|
||||
return a, nil
|
||||
}
|
||||
return a, nil
|
||||
case key.Matches(msg, logsKeyReturnKey):
|
||||
if a.currentPage == page.LogsPage {
|
||||
return a, a.moveToPage(page.ChatPage)
|
||||
@@ -255,6 +343,14 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
a.showHelp = !a.showHelp
|
||||
return a, nil
|
||||
}
|
||||
if a.showInitDialog {
|
||||
a.showInitDialog = false
|
||||
// Mark the project as initialized without running the command
|
||||
if err := config.MarkProjectInitialized(); err != nil {
|
||||
return a, util.ReportError(err)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
case key.Matches(msg, keys.Logs):
|
||||
return a, a.moveToPage(page.LogsPage)
|
||||
case key.Matches(msg, keys.Help):
|
||||
@@ -304,6 +400,26 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
|
||||
if a.showCommandDialog {
|
||||
d, commandCmd := a.commandDialog.Update(msg)
|
||||
a.commandDialog = d.(dialog.CommandDialog)
|
||||
cmds = append(cmds, commandCmd)
|
||||
// Only block key messages send all other messages down
|
||||
if _, ok := msg.(tea.KeyMsg); ok {
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
if a.showInitDialog {
|
||||
d, initCmd := a.initDialog.Update(msg)
|
||||
a.initDialog = d.(dialog.InitDialogCmp)
|
||||
cmds = append(cmds, initCmd)
|
||||
// Only block key messages send all other messages down
|
||||
if _, ok := msg.(tea.KeyMsg); ok {
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
s, _ := a.status.Update(msg)
|
||||
a.status = s.(core.StatusCmp)
|
||||
a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
|
||||
@@ -311,6 +427,11 @@ 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 {
|
||||
if a.app.CoderAgent.IsBusy() {
|
||||
// For now we don't move to any page if the agent is busy
|
||||
@@ -422,24 +543,74 @@ func (a appModel) View() string {
|
||||
)
|
||||
}
|
||||
|
||||
if a.showCommandDialog {
|
||||
overlay := a.commandDialog.View()
|
||||
row := lipgloss.Height(appView) / 2
|
||||
row -= lipgloss.Height(overlay) / 2
|
||||
col := lipgloss.Width(appView) / 2
|
||||
col -= lipgloss.Width(overlay) / 2
|
||||
appView = layout.PlaceOverlay(
|
||||
col,
|
||||
row,
|
||||
overlay,
|
||||
appView,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
if a.showInitDialog {
|
||||
overlay := a.initDialog.View()
|
||||
appView = layout.PlaceOverlay(
|
||||
a.width/2-lipgloss.Width(overlay)/2,
|
||||
a.height/2-lipgloss.Height(overlay)/2,
|
||||
overlay,
|
||||
appView,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
return appView
|
||||
}
|
||||
|
||||
func New(app *app.App) tea.Model {
|
||||
startPage := page.ChatPage
|
||||
return &appModel{
|
||||
model := &appModel{
|
||||
currentPage: startPage,
|
||||
loadedPages: make(map[page.PageID]bool),
|
||||
status: core.NewStatusCmp(app.LSPClients),
|
||||
help: dialog.NewHelpCmp(),
|
||||
quit: dialog.NewQuitCmp(),
|
||||
sessionDialog: dialog.NewSessionDialogCmp(),
|
||||
commandDialog: dialog.NewCommandDialogCmp(),
|
||||
permissions: dialog.NewPermissionDialogCmp(),
|
||||
initDialog: dialog.NewInitDialogCmp(),
|
||||
app: app,
|
||||
editingMode: true,
|
||||
commands: []dialog.Command{},
|
||||
pages: map[page.PageID]tea.Model{
|
||||
page.ChatPage: page.NewChatPage(app),
|
||||
page.LogsPage: page.NewLogsPage(),
|
||||
},
|
||||
}
|
||||
|
||||
model.RegisterCommand(dialog.Command{
|
||||
ID: "init",
|
||||
Title: "Initialize Project",
|
||||
Description: "Create/Update the OpenCode.md memory file",
|
||||
Handler: func(cmd dialog.Command) tea.Cmd {
|
||||
prompt := `Please analyze this codebase and create a OpenCode.md file containing:
|
||||
1. Build/lint/test commands - especially for running a single test
|
||||
2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
|
||||
|
||||
The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
|
||||
If there's already a opencode.md, improve it.
|
||||
If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.`
|
||||
return tea.Batch(
|
||||
util.CmdHandler(chat.SendMsg{
|
||||
Text: prompt,
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
return model
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user