| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971 |
- package tui
- import (
- "context"
- "log/slog"
- "strings"
- "github.com/charmbracelet/bubbles/cursor"
- "github.com/charmbracelet/bubbles/key"
- "github.com/charmbracelet/bubbles/spinner"
- tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/lipgloss"
- "github.com/sst/opencode/internal/config"
- "github.com/sst/opencode/internal/tui/app"
- "github.com/sst/opencode/internal/status"
- "github.com/sst/opencode/internal/tui/components/chat"
- "github.com/sst/opencode/internal/tui/components/core"
- "github.com/sst/opencode/internal/tui/components/dialog"
- "github.com/sst/opencode/internal/tui/layout"
- "github.com/sst/opencode/internal/tui/page"
- "github.com/sst/opencode/internal/tui/state"
- "github.com/sst/opencode/internal/tui/util"
- "github.com/sst/opencode/pkg/client"
- )
- type keyMap struct {
- Quit key.Binding
- Help key.Binding
- SwitchSession key.Binding
- Commands key.Binding
- Filepicker key.Binding
- Models key.Binding
- SwitchTheme key.Binding
- Tools key.Binding
- }
- const (
- quitKey = "q"
- )
- var keys = keyMap{
- Quit: key.NewBinding(
- key.WithKeys("ctrl+c"),
- key.WithHelp("ctrl+c", "quit"),
- ),
- Help: key.NewBinding(
- key.WithKeys("ctrl+_"),
- key.WithHelp("ctrl+?", "toggle help"),
- ),
- SwitchSession: key.NewBinding(
- key.WithKeys("ctrl+s"),
- key.WithHelp("ctrl+s", "switch session"),
- ),
- Commands: key.NewBinding(
- key.WithKeys("ctrl+k"),
- key.WithHelp("ctrl+k", "commands"),
- ),
- Filepicker: key.NewBinding(
- key.WithKeys("ctrl+f"),
- key.WithHelp("ctrl+f", "select files to upload"),
- ),
- Models: key.NewBinding(
- key.WithKeys("ctrl+o"),
- key.WithHelp("ctrl+o", "model selection"),
- ),
- SwitchTheme: key.NewBinding(
- key.WithKeys("ctrl+t"),
- key.WithHelp("ctrl+t", "switch theme"),
- ),
- Tools: key.NewBinding(
- key.WithKeys("f9"),
- key.WithHelp("f9", "show available tools"),
- ),
- }
- var helpEsc = key.NewBinding(
- key.WithKeys("?"),
- key.WithHelp("?", "toggle help"),
- )
- var returnKey = key.NewBinding(
- key.WithKeys("esc"),
- key.WithHelp("esc", "close"),
- )
- type appModel struct {
- width, height int
- currentPage page.PageID
- previousPage page.PageID
- pages map[page.PageID]tea.Model
- loadedPages map[page.PageID]bool
- status core.StatusCmp
- app *app.App
- showPermissions bool
- permissions dialog.PermissionDialogCmp
- showHelp bool
- help dialog.HelpCmp
- showQuit bool
- quit dialog.QuitDialog
- showSessionDialog bool
- sessionDialog dialog.SessionDialog
- showCommandDialog bool
- commandDialog dialog.CommandDialog
- commands []dialog.Command
- showModelDialog bool
- modelDialog dialog.ModelDialog
- showInitDialog bool
- initDialog dialog.InitDialogCmp
- showFilepicker bool
- filepicker dialog.FilepickerCmp
- showThemeDialog bool
- themeDialog dialog.ThemeDialog
- showMultiArgumentsDialog bool
- multiArgumentsDialog dialog.MultiArgumentsDialogCmp
- showToolsDialog bool
- toolsDialog dialog.ToolsDialog
- }
- func (a appModel) Init() tea.Cmd {
- var cmds []tea.Cmd
- cmd := a.pages[a.currentPage].Init()
- a.loadedPages[a.currentPage] = true
- cmds = append(cmds, cmd)
- cmd = a.status.Init()
- cmds = append(cmds, cmd)
- cmd = a.quit.Init()
- cmds = append(cmds, cmd)
- cmd = a.help.Init()
- cmds = append(cmds, cmd)
- cmd = a.sessionDialog.Init()
- cmds = append(cmds, cmd)
- cmd = a.commandDialog.Init()
- cmds = append(cmds, cmd)
- cmd = a.modelDialog.Init()
- cmds = append(cmds, cmd)
- cmd = a.initDialog.Init()
- cmds = append(cmds, cmd)
- cmd = a.filepicker.Init()
- cmds = append(cmds, cmd)
- cmd = a.themeDialog.Init()
- cmds = append(cmds, cmd)
- cmd = a.toolsDialog.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 {
- status.Error("Failed to check init status: " + err.Error())
- return nil
- }
- return dialog.ShowInitDialogMsg{Show: shouldShow}
- })
- return tea.Batch(cmds...)
- }
- func (a appModel) updateAllPages(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmds []tea.Cmd
- var cmd tea.Cmd
- for id := range a.pages {
- a.pages[id], cmd = a.pages[id].Update(msg)
- cmds = append(cmds, cmd)
- }
- return a, tea.Batch(cmds...)
- }
- func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmds []tea.Cmd
- var cmd tea.Cmd
- switch msg := msg.(type) {
- case cursor.BlinkMsg:
- return a.updateAllPages(msg)
- case spinner.TickMsg:
- return a.updateAllPages(msg)
- case client.EventSessionUpdated:
- if msg.Properties.Info.Id == a.app.Session.Id {
- a.app.Session = &msg.Properties.Info
- return a.updateAllPages(state.StateUpdatedMsg{State: nil})
- }
- case client.EventMessageUpdated:
- if msg.Properties.Info.Metadata.SessionID == a.app.Session.Id {
- for i, m := range a.app.Messages {
- if m.Id == msg.Properties.Info.Id {
- a.app.Messages[i] = msg.Properties.Info
- slog.Debug("Updated message", "message", msg.Properties.Info)
- return a.updateAllPages(state.StateUpdatedMsg{State: nil})
- }
- }
- a.app.Messages = append(a.app.Messages, msg.Properties.Info)
- slog.Debug("Appended message", "message", msg.Properties.Info)
- return a.updateAllPages(state.StateUpdatedMsg{State: nil})
- }
- case tea.WindowSizeMsg:
- msg.Height -= 2 // Make space for the status bar
- a.width, a.height = msg.Width, msg.Height
- s, _ := a.status.Update(msg)
- a.status = s.(core.StatusCmp)
- a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
- cmds = append(cmds, cmd)
- prm, permCmd := a.permissions.Update(msg)
- a.permissions = prm.(dialog.PermissionDialogCmp)
- cmds = append(cmds, permCmd)
- help, helpCmd := a.help.Update(msg)
- a.help = help.(dialog.HelpCmp)
- cmds = append(cmds, helpCmd)
- session, sessionCmd := a.sessionDialog.Update(msg)
- a.sessionDialog = session.(dialog.SessionDialog)
- cmds = append(cmds, sessionCmd)
- command, commandCmd := a.commandDialog.Update(msg)
- a.commandDialog = command.(dialog.CommandDialog)
- cmds = append(cmds, commandCmd)
- filepicker, filepickerCmd := a.filepicker.Update(msg)
- a.filepicker = filepicker.(dialog.FilepickerCmp)
- cmds = append(cmds, filepickerCmd)
- a.initDialog.SetSize(msg.Width, msg.Height)
- if a.showMultiArgumentsDialog {
- a.multiArgumentsDialog.SetSize(msg.Width, msg.Height)
- args, argsCmd := a.multiArgumentsDialog.Update(msg)
- a.multiArgumentsDialog = args.(dialog.MultiArgumentsDialogCmp)
- cmds = append(cmds, argsCmd, a.multiArgumentsDialog.Init())
- }
- return a, tea.Batch(cmds...)
- // case pubsub.Event[permission.PermissionRequest]:
- // a.showPermissions = true
- // return a, a.permissions.SetPermissions(msg.Payload)
- case dialog.PermissionResponseMsg:
- // TODO: Permissions service not implemented in API yet
- // var cmd tea.Cmd
- // switch msg.Action {
- // case dialog.PermissionAllow:
- // a.app.Permissions.Grant(context.Background(), msg.Permission)
- // case dialog.PermissionAllowForSession:
- // a.app.Permissions.GrantPersistant(context.Background(), msg.Permission)
- // case dialog.PermissionDeny:
- // a.app.Permissions.Deny(context.Background(), msg.Permission)
- // }
- a.showPermissions = false
- return a, nil
- case page.PageChangeMsg:
- return a, a.moveToPage(msg.ID)
- case dialog.CloseQuitMsg:
- a.showQuit = false
- return a, nil
- case dialog.CloseSessionDialogMsg:
- a.showSessionDialog = false
- if msg.Session != nil {
- return a, util.CmdHandler(state.SessionSelectedMsg(msg.Session))
- }
- return a, nil
- case state.SessionSelectedMsg:
- a.app.Session = msg
- a.app.Messages, _ = a.app.ListMessages(context.Background(), msg.Id)
- return a.updateAllPages(msg)
- case dialog.CloseCommandDialogMsg:
- a.showCommandDialog = false
- return a, nil
- case dialog.CloseThemeDialogMsg:
- a.showThemeDialog = false
- return a, nil
- case dialog.CloseToolsDialogMsg:
- a.showToolsDialog = false
- return a, nil
- case dialog.ShowToolsDialogMsg:
- a.showToolsDialog = msg.Show
- return a, nil
- case dialog.ThemeChangedMsg:
- a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
- a.showThemeDialog = false
- status.Info("Theme changed to: " + msg.ThemeName)
- return a, cmd
- case dialog.CloseModelDialogMsg:
- a.showModelDialog = false
- return a, nil
- case dialog.ModelSelectedMsg:
- a.showModelDialog = false
- // TODO: Agent model update not implemented in API yet
- // model, err := a.app.PrimaryAgent.Update(config.AgentPrimary, msg.Model.ID)
- // if err != nil {
- // status.Error(err.Error())
- // return a, nil
- // }
- // status.Info(fmt.Sprintf("Model changed to %s", model.Name))
- status.Info("Model selection not implemented in API yet")
- 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 {
- status.Error(err.Error())
- return a, nil
- }
- return a, cmd.Handler(cmd)
- }
- }
- } else {
- // Mark the project as initialized without running the command
- if err := config.MarkProjectInitialized(); err != nil {
- status.Error(err.Error())
- return a, nil
- }
- }
- 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)
- }
- status.Info("Command selected: " + msg.Command.Title)
- return a, nil
- case dialog.ShowMultiArgumentsDialogMsg:
- // Show multi-arguments dialog
- a.multiArgumentsDialog = dialog.NewMultiArgumentsDialogCmp(msg.CommandID, msg.Content, msg.ArgNames)
- a.showMultiArgumentsDialog = true
- return a, a.multiArgumentsDialog.Init()
- case dialog.CloseMultiArgumentsDialogMsg:
- // Close multi-arguments dialog
- a.showMultiArgumentsDialog = false
- // If submitted, replace all named arguments and run the command
- if msg.Submit {
- content := msg.Content
- // Replace each named argument with its value
- for name, value := range msg.Args {
- placeholder := "$" + name
- content = strings.ReplaceAll(content, placeholder, value)
- }
- // Execute the command with arguments
- return a, util.CmdHandler(dialog.CommandRunCustomMsg{
- Content: content,
- Args: msg.Args,
- })
- }
- return a, nil
- case tea.KeyMsg:
- // If multi-arguments dialog is open, let it handle the key press first
- if a.showMultiArgumentsDialog {
- args, cmd := a.multiArgumentsDialog.Update(msg)
- a.multiArgumentsDialog = args.(dialog.MultiArgumentsDialogCmp)
- return a, cmd
- }
- switch {
- case key.Matches(msg, keys.Quit):
- a.showQuit = !a.showQuit
- if a.showHelp {
- a.showHelp = false
- }
- if a.showSessionDialog {
- a.showSessionDialog = false
- }
- if a.showCommandDialog {
- a.showCommandDialog = false
- }
- if a.showFilepicker {
- a.showFilepicker = false
- a.filepicker.ToggleFilepicker(a.showFilepicker)
- a.app.SetFilepickerOpen(a.showFilepicker)
- }
- if a.showModelDialog {
- a.showModelDialog = false
- }
- if a.showMultiArgumentsDialog {
- a.showMultiArgumentsDialog = false
- }
- if a.showToolsDialog {
- a.showToolsDialog = false
- }
- return a, nil
- case key.Matches(msg, keys.SwitchSession):
- if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showCommandDialog {
- // Close other dialogs
- a.showToolsDialog = false
- a.showThemeDialog = false
- a.showModelDialog = false
- a.showFilepicker = false
- // Load sessions and show the dialog
- sessions, err := a.app.ListSessions(context.Background())
- if err != nil {
- status.Error(err.Error())
- return a, nil
- }
- if len(sessions) == 0 {
- status.Warn("No sessions available")
- return a, nil
- }
- a.sessionDialog.SetSessions(sessions)
- a.showSessionDialog = true
- return a, nil
- }
- return a, nil
- case key.Matches(msg, keys.Commands):
- if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showThemeDialog && !a.showFilepicker {
- // Close other dialogs
- a.showToolsDialog = false
- a.showModelDialog = false
- // Show commands dialog
- if len(a.commands) == 0 {
- status.Warn("No commands available")
- return a, nil
- }
- a.commandDialog.SetCommands(a.commands)
- a.showCommandDialog = true
- return a, nil
- }
- return a, nil
- case key.Matches(msg, keys.Models):
- if a.showModelDialog {
- a.showModelDialog = false
- return a, nil
- }
- if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
- // Close other dialogs
- a.showToolsDialog = false
- a.showThemeDialog = false
- a.showFilepicker = false
- a.showModelDialog = true
- return a, nil
- }
- return a, nil
- case key.Matches(msg, keys.SwitchTheme):
- if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
- // Close other dialogs
- a.showToolsDialog = false
- a.showModelDialog = false
- a.showFilepicker = false
- a.showThemeDialog = true
- return a, a.themeDialog.Init()
- }
- return a, nil
- case key.Matches(msg, keys.Tools):
- // Check if any other dialog is open
- if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions &&
- !a.showSessionDialog && !a.showCommandDialog && !a.showThemeDialog &&
- !a.showFilepicker && !a.showModelDialog && !a.showInitDialog &&
- !a.showMultiArgumentsDialog {
- // Toggle tools dialog
- a.showToolsDialog = !a.showToolsDialog
- if a.showToolsDialog {
- // Get tool names dynamically
- toolNames := getAvailableToolNames(a.app)
- a.toolsDialog.SetTools(toolNames)
- }
- return a, nil
- }
- return a, nil
- case key.Matches(msg, returnKey) || key.Matches(msg):
- if !a.filepicker.IsCWDFocused() {
- if a.showToolsDialog {
- a.showToolsDialog = false
- return a, nil
- }
- if a.showQuit {
- a.showQuit = !a.showQuit
- return a, nil
- }
- if a.showHelp {
- 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 {
- status.Error(err.Error())
- return a, nil
- }
- return a, nil
- }
- if a.showFilepicker {
- a.showFilepicker = false
- a.filepicker.ToggleFilepicker(a.showFilepicker)
- a.app.SetFilepickerOpen(a.showFilepicker)
- return a, nil
- }
- }
- case key.Matches(msg, keys.Help):
- if a.showQuit {
- return a, nil
- }
- a.showHelp = !a.showHelp
- // Close other dialogs if opening help
- if a.showHelp {
- a.showToolsDialog = false
- }
- return a, nil
- case key.Matches(msg, helpEsc):
- if a.app.PrimaryAgentOLD.IsBusy() {
- if a.showQuit {
- return a, nil
- }
- a.showHelp = !a.showHelp
- return a, nil
- }
- case key.Matches(msg, keys.Filepicker):
- // Toggle filepicker
- a.showFilepicker = !a.showFilepicker
- a.filepicker.ToggleFilepicker(a.showFilepicker)
- a.app.SetFilepickerOpen(a.showFilepicker)
- // Close other dialogs if opening filepicker
- if a.showFilepicker {
- a.showToolsDialog = false
- a.showThemeDialog = false
- a.showModelDialog = false
- a.showCommandDialog = false
- a.showSessionDialog = false
- }
- return a, nil
- }
- default:
- f, filepickerCmd := a.filepicker.Update(msg)
- a.filepicker = f.(dialog.FilepickerCmp)
- cmds = append(cmds, filepickerCmd)
- }
- if a.showFilepicker {
- f, filepickerCmd := a.filepicker.Update(msg)
- a.filepicker = f.(dialog.FilepickerCmp)
- cmds = append(cmds, filepickerCmd)
- // Only block key messages send all other messages down
- if _, ok := msg.(tea.KeyMsg); ok {
- return a, tea.Batch(cmds...)
- }
- }
- if a.showQuit {
- q, quitCmd := a.quit.Update(msg)
- a.quit = q.(dialog.QuitDialog)
- cmds = append(cmds, quitCmd)
- // Only block key messages send all other messages down
- if _, ok := msg.(tea.KeyMsg); ok {
- return a, tea.Batch(cmds...)
- }
- }
- if a.showPermissions {
- d, permissionsCmd := a.permissions.Update(msg)
- a.permissions = d.(dialog.PermissionDialogCmp)
- cmds = append(cmds, permissionsCmd)
- // Only block key messages send all other messages down
- if _, ok := msg.(tea.KeyMsg); ok {
- return a, tea.Batch(cmds...)
- }
- }
- if a.showSessionDialog {
- d, sessionCmd := a.sessionDialog.Update(msg)
- a.sessionDialog = d.(dialog.SessionDialog)
- cmds = append(cmds, sessionCmd)
- // Only block key messages send all other messages down
- if _, ok := msg.(tea.KeyMsg); ok {
- return a, tea.Batch(cmds...)
- }
- }
- 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.showModelDialog {
- d, modelCmd := a.modelDialog.Update(msg)
- a.modelDialog = d.(dialog.ModelDialog)
- cmds = append(cmds, modelCmd)
- // 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...)
- }
- }
- if a.showThemeDialog {
- d, themeCmd := a.themeDialog.Update(msg)
- a.themeDialog = d.(dialog.ThemeDialog)
- cmds = append(cmds, themeCmd)
- // Only block key messages send all other messages down
- if _, ok := msg.(tea.KeyMsg); ok {
- return a, tea.Batch(cmds...)
- }
- }
- if a.showToolsDialog {
- d, toolsCmd := a.toolsDialog.Update(msg)
- a.toolsDialog = d.(dialog.ToolsDialog)
- cmds = append(cmds, toolsCmd)
- // Only block key messages send all other messages down
- if _, ok := msg.(tea.KeyMsg); ok {
- return a, tea.Batch(cmds...)
- }
- }
- s, cmd := a.status.Update(msg)
- cmds = append(cmds, cmd)
- a.status = s.(core.StatusCmp)
- a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
- cmds = append(cmds, 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)
- }
- // getAvailableToolNames returns a list of all available tool names
- func getAvailableToolNames(_ *app.App) []string {
- // TODO: Tools not implemented in API yet
- return []string{"Tools not available in API mode"}
- }
- func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
- var cmds []tea.Cmd
- if _, ok := a.loadedPages[pageID]; !ok {
- cmd := a.pages[pageID].Init()
- cmds = append(cmds, cmd)
- a.loadedPages[pageID] = true
- }
- a.previousPage = a.currentPage
- a.currentPage = pageID
- if sizable, ok := a.pages[a.currentPage].(layout.Sizeable); ok {
- cmd := sizable.SetSize(a.width, a.height)
- cmds = append(cmds, cmd)
- }
- return tea.Batch(cmds...)
- }
- func (a appModel) View() string {
- components := []string{
- a.pages[a.currentPage].View(),
- }
- components = append(components, a.status.View())
- appView := lipgloss.JoinVertical(lipgloss.Top, components...)
- if a.showPermissions {
- overlay := a.permissions.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.showFilepicker {
- overlay := a.filepicker.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.app.PrimaryAgentOLD.IsBusy() {
- a.status.SetHelpWidgetMsg("ctrl+? help")
- } else {
- a.status.SetHelpWidgetMsg("? help")
- }
- if a.showHelp {
- bindings := layout.KeyMapToSlice(keys)
- if p, ok := a.pages[a.currentPage].(layout.Bindings); ok {
- bindings = append(bindings, p.BindingKeys()...)
- }
- if a.showPermissions {
- bindings = append(bindings, a.permissions.BindingKeys()...)
- }
- if !a.app.PrimaryAgentOLD.IsBusy() {
- bindings = append(bindings, helpEsc)
- }
- a.help.SetBindings(bindings)
- overlay := a.help.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.showQuit {
- overlay := a.quit.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.showSessionDialog {
- overlay := a.sessionDialog.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.showModelDialog {
- overlay := a.modelDialog.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.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,
- )
- }
- if a.showThemeDialog {
- overlay := a.themeDialog.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.showMultiArgumentsDialog {
- overlay := a.multiArgumentsDialog.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.showToolsDialog {
- overlay := a.toolsDialog.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,
- )
- }
- return appView
- }
- func New(app *app.App) tea.Model {
- startPage := page.ChatPage
- model := &appModel{
- currentPage: startPage,
- loadedPages: make(map[page.PageID]bool),
- status: core.NewStatusCmp(app),
- help: dialog.NewHelpCmp(),
- quit: dialog.NewQuitCmp(),
- sessionDialog: dialog.NewSessionDialogCmp(),
- commandDialog: dialog.NewCommandDialogCmp(),
- modelDialog: dialog.NewModelDialogCmp(),
- permissions: dialog.NewPermissionDialogCmp(),
- initDialog: dialog.NewInitDialogCmp(),
- themeDialog: dialog.NewThemeDialogCmp(),
- toolsDialog: dialog.NewToolsDialogCmp(),
- app: app,
- commands: []dialog.Command{},
- pages: map[page.PageID]tea.Model{
- page.ChatPage: page.NewChatPage(app),
- },
- filepicker: dialog.NewFilepickerCmp(app),
- }
- model.RegisterCommand(dialog.Command{
- ID: "init",
- Title: "Initialize Project",
- Description: "Create/Update the CONTEXT.md memory file",
- Handler: func(cmd dialog.Command) tea.Cmd {
- prompt := `Please analyze this codebase and create a CONTEXT.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 CONTEXT.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,
- }),
- )
- },
- })
- model.RegisterCommand(dialog.Command{
- ID: "compact_conversation",
- Title: "Compact Conversation",
- Description: "Summarize the current session to save tokens",
- Handler: func(cmd dialog.Command) tea.Cmd {
- // Get the current session from the appModel
- if model.currentPage != page.ChatPage {
- status.Warn("Please navigate to a chat session first.")
- return nil
- }
- // Return a message that will be handled by the chat page
- status.Info("Compacting conversation...")
- return util.CmdHandler(state.CompactSessionMsg{})
- },
- })
- // Load custom commands
- customCommands, err := dialog.LoadCustomCommands()
- if err != nil {
- slog.Warn("Failed to load custom commands", "error", err)
- } else {
- for _, cmd := range customCommands {
- model.RegisterCommand(cmd)
- }
- }
- return model
- }
|