diff options
Diffstat (limited to 'internal/tui/tui.go')
| -rw-r--r-- | internal/tui/tui.go | 175 |
1 files changed, 173 insertions, 2 deletions
diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 392b9ec41..4a723d40d 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -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 } |
