@@ -7,6 +7,7 @@ import (
7 tea "github.com/charmbracelet/bubbletea"
7 tea "github.com/charmbracelet/bubbletea"
8 "github.com/charmbracelet/lipgloss"
8 "github.com/charmbracelet/lipgloss"
9 "github.com/kujtimiihoxha/opencode/internal/app"
9 "github.com/kujtimiihoxha/opencode/internal/app"
10+ "github.com/kujtimiihoxha/opencode/internal/config"
10 "github.com/kujtimiihoxha/opencode/internal/logging"
11 "github.com/kujtimiihoxha/opencode/internal/logging"
11 "github.com/kujtimiihoxha/opencode/internal/permission"
12 "github.com/kujtimiihoxha/opencode/internal/permission"
12 "github.com/kujtimiihoxha/opencode/internal/pubsub"
13 "github.com/kujtimiihoxha/opencode/internal/pubsub"
@@ -23,6 +24,7 @@ type keyMap struct {
23 Quit key.Binding
24 Quit key.Binding
24 Help key.Binding
25 Help key.Binding
25 SwitchSession key.Binding
26 SwitchSession key.Binding
28var keys = keyMap{
30var keys = keyMap{
@@ -44,6 +46,11 @@ var keys = keyMap{
44 key.WithKeys("ctrl+a"),
46 key.WithKeys("ctrl+a"),
45 key.WithHelp("ctrl+a", "switch session"),
47 key.WithHelp("ctrl+a", "switch session"),
50+ Commands: key.NewBinding(
51+ key.WithKeys("ctrl+k"),
52+ key.WithHelp("ctrl+K", "commands"),
49var helpEsc = key.NewBinding(
56var helpEsc = key.NewBinding(
@@ -82,6 +89,13 @@ type appModel struct {
82 showSessionDialog bool
89 showSessionDialog bool
83 sessionDialog dialog.SessionDialog
90 sessionDialog dialog.SessionDialog
92+ showCommandDialog bool
93+ commandDialog dialog.CommandDialog
94+ commands []dialog.Command
97+ initDialog dialog.InitDialogCmp
85 editingMode bool
99 editingMode bool
@@ -98,6 +112,23 @@ func (a appModel) Init() tea.Cmd {
98 cmds = append(cmds, cmd)
112 cmds = append(cmds, cmd)
99 cmd = a.sessionDialog.Init()
113 cmd = a.sessionDialog.Init()
100 cmds = append(cmds, cmd)
114 cmds = append(cmds, cmd)
115+ cmd = a.commandDialog.Init()
116+ cmds = append(cmds, cmd)
117+ cmd = a.initDialog.Init()
118+ cmds = append(cmds, cmd)
120+ // Check if we should show the init dialog
121+ cmds = append(cmds, func() tea.Msg {
122+ shouldShow, err := config.ShouldShowInitDialog()
124+ return util.InfoMsg{
125+ Type: util.InfoTypeError,
126+ Msg: "Failed to check init status: " + err.Error(),
129+ return dialog.ShowInitDialogMsg{Show: shouldShow}
101 return tea.Batch(cmds...)
132 return tea.Batch(cmds...)
@@ -126,6 +157,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
126 a.sessionDialog = session.(dialog.SessionDialog)
157 a.sessionDialog = session.(dialog.SessionDialog)
127 cmds = append(cmds, sessionCmd)
158 cmds = append(cmds, sessionCmd)
160+ command, commandCmd := a.commandDialog.Update(msg)
161+ a.commandDialog = command.(dialog.CommandDialog)
162+ cmds = append(cmds, commandCmd)
164+ a.initDialog.SetSize(msg.Width, msg.Height)
129 return a, tea.Batch(cmds...)
166 return a, tea.Batch(cmds...)
130 case chat.EditorFocusMsg:
167 case chat.EditorFocusMsg:
131 a.editingMode = bool(msg)
168 a.editingMode = bool(msg)
@@ -207,6 +244,35 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
207 a.showSessionDialog = false
244 a.showSessionDialog = false
208 return a, nil
245 return a, nil
247+ case dialog.CloseCommandDialogMsg:
248+ a.showCommandDialog = false
251+ case dialog.ShowInitDialogMsg:
252+ a.showInitDialog = msg.Show
255+ case dialog.CloseInitDialogMsg:
256+ a.showInitDialog = false
258+ // Run the initialization command
259+ for _, cmd := range a.commands {
260+ if cmd.ID == "init" {
261+ // Mark the project as initialized
262+ if err := config.MarkProjectInitialized(); err != nil {
263+ return a, util.ReportError(err)
265+ return a, cmd.Handler(cmd)
269+ // Mark the project as initialized without running the command
270+ if err := config.MarkProjectInitialized(); err != nil {
271+ return a, util.ReportError(err)
210 case chat.SessionSelectedMsg:
276 case chat.SessionSelectedMsg:
211 a.sessionDialog.SetSelectedSession(msg.ID)
277 a.sessionDialog.SetSelectedSession(msg.ID)
212 case dialog.SessionSelectedMsg:
278 case dialog.SessionSelectedMsg:
@@ -216,6 +282,14 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
217 return a, nil
283 return a, nil
285+ case dialog.CommandSelectedMsg:
286+ a.showCommandDialog = false
287+ // Execute the command handler if available
288+ if msg.Command.Handler != nil {
289+ return a, msg.Command.Handler(msg.Command)
291+ return a, util.ReportInfo("Command selected: " + msg.Command.Title)
219 case tea.KeyMsg:
293 case tea.KeyMsg:
221 case key.Matches(msg, keys.Quit):
295 case key.Matches(msg, keys.Quit):
@@ -226,9 +300,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
226 if a.showSessionDialog {
300 if a.showSessionDialog {
227 a.showSessionDialog = false
301 a.showSessionDialog = false
303+ if a.showCommandDialog {
304+ a.showCommandDialog = false
229 return a, nil
306 return a, nil
230 case key.Matches(msg, keys.SwitchSession):
307 case key.Matches(msg, keys.SwitchSession):
231− if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions {
308+ if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showCommandDialog {
232 // Load sessions and show the dialog
309 // Load sessions and show the dialog
233 sessions, err := a.app.Sessions.List(context.Background())
310 sessions, err := a.app.Sessions.List(context.Background())
234 if err != nil {
311 if err != nil {
@@ -242,6 +319,17 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
242 return a, nil
319 return a, nil
244 return a, nil
321 return a, nil
322+ case key.Matches(msg, keys.Commands):
323+ if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog {
324+ // Show commands dialog
325+ if len(a.commands) == 0 {
326+ return a, util.ReportWarn("No commands available")
328+ a.commandDialog.SetCommands(a.commands)
329+ a.showCommandDialog = true
245 case key.Matches(msg, logsKeyReturnKey):
333 case key.Matches(msg, logsKeyReturnKey):
246 if a.currentPage == page.LogsPage {
334 if a.currentPage == page.LogsPage {
247 return a, a.moveToPage(page.ChatPage)
335 return a, a.moveToPage(page.ChatPage)
@@ -255,6 +343,14 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
255 a.showHelp = !a.showHelp
343 a.showHelp = !a.showHelp
256 return a, nil
344 return a, nil
346+ if a.showInitDialog {
347+ a.showInitDialog = false
348+ // Mark the project as initialized without running the command
349+ if err := config.MarkProjectInitialized(); err != nil {
350+ return a, util.ReportError(err)
258 case key.Matches(msg, keys.Logs):
354 case key.Matches(msg, keys.Logs):
259 return a, a.moveToPage(page.LogsPage)
355 return a, a.moveToPage(page.LogsPage)
260 case key.Matches(msg, keys.Help):
356 case key.Matches(msg, keys.Help):
@@ -304,6 +400,26 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
403+ if a.showCommandDialog {
404+ d, commandCmd := a.commandDialog.Update(msg)
405+ a.commandDialog = d.(dialog.CommandDialog)
406+ cmds = append(cmds, commandCmd)
407+ // Only block key messages send all other messages down
408+ if _, ok := msg.(tea.KeyMsg); ok {
409+ return a, tea.Batch(cmds...)
413+ if a.showInitDialog {
414+ d, initCmd := a.initDialog.Update(msg)
415+ a.initDialog = d.(dialog.InitDialogCmp)
416+ cmds = append(cmds, initCmd)
417+ // Only block key messages send all other messages down
418+ if _, ok := msg.(tea.KeyMsg); ok {
419+ return a, tea.Batch(cmds...)
307 s, _ := a.status.Update(msg)
423 s, _ := a.status.Update(msg)
308 a.status = s.(core.StatusCmp)
424 a.status = s.(core.StatusCmp)
309 a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
425 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) {
311 return a, tea.Batch(cmds...)
427 return a, tea.Batch(cmds...)
430+// RegisterCommand adds a command to the command dialog
431+func (a *appModel) RegisterCommand(cmd dialog.Command) {
432+ a.commands = append(a.commands, cmd)
314func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
435func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
315 if a.app.CoderAgent.IsBusy() {
436 if a.app.CoderAgent.IsBusy() {
316 // For now we don't move to any page if the agent is busy
437 // For now we don't move to any page if the agent is busy
@@ -422,24 +543,74 @@ func (a appModel) View() string {
546+ if a.showCommandDialog {
547+ overlay := a.commandDialog.View()
548+ row := lipgloss.Height(appView) / 2
549+ row -= lipgloss.Height(overlay) / 2
550+ col := lipgloss.Width(appView) / 2
551+ col -= lipgloss.Width(overlay) / 2
552+ appView = layout.PlaceOverlay(
561+ if a.showInitDialog {
562+ overlay := a.initDialog.View()
563+ appView = layout.PlaceOverlay(
564+ a.width/2-lipgloss.Width(overlay)/2,
565+ a.height/2-lipgloss.Height(overlay)/2,
425 return appView
572 return appView
428func New(app *app.App) tea.Model {
575func New(app *app.App) tea.Model {
429 startPage := page.ChatPage
576 startPage := page.ChatPage
430− return &appModel{
577+ model := &appModel{
431 currentPage: startPage,
578 currentPage: startPage,
432 loadedPages: make(map[page.PageID]bool),
579 loadedPages: make(map[page.PageID]bool),
433 status: core.NewStatusCmp(app.LSPClients),
580 status: core.NewStatusCmp(app.LSPClients),
434 help: dialog.NewHelpCmp(),
581 help: dialog.NewHelpCmp(),
435 quit: dialog.NewQuitCmp(),
582 quit: dialog.NewQuitCmp(),
436 sessionDialog: dialog.NewSessionDialogCmp(),
583 sessionDialog: dialog.NewSessionDialogCmp(),
584+ commandDialog: dialog.NewCommandDialogCmp(),
437 permissions: dialog.NewPermissionDialogCmp(),
585 permissions: dialog.NewPermissionDialogCmp(),
586+ initDialog: dialog.NewInitDialogCmp(),
438 app: app,
587 app: app,
439 editingMode: true,
588 editingMode: true,
589+ commands: []dialog.Command{},
440 pages: map[page.PageID]tea.Model{
590 pages: map[page.PageID]tea.Model{
441 page.ChatPage: page.NewChatPage(app),
591 page.ChatPage: page.NewChatPage(app),
442 page.LogsPage: page.NewLogsPage(),
592 page.LogsPage: page.NewLogsPage(),
596+ model.RegisterCommand(dialog.Command{
598+ Title: "Initialize Project",
599+ Description: "Create/Update the OpenCode.md memory file",
600+ Handler: func(cmd dialog.Command) tea.Cmd {
601+ prompt := `Please analyze this codebase and create a OpenCode.md file containing:
602+1. Build/lint/test commands - especially for running a single test
603+2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
605+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.
606+If there's already a opencode.md, improve it.
607+If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.`
609+ util.CmdHandler(chat.SendMsg{