From 967bf95e2f4bd892db3cce243ec4cb5073b40004 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Mon, 16 Jun 2025 13:37:15 +0200 Subject: [PATCH] feat: implement show all help --- internal/tui/components/chat/chat.go | 20 ++++-- internal/tui/components/chat/editor/editor.go | 7 +- internal/tui/components/chat/editor/keys.go | 31 +++------ .../tui/components/core/layout/container.go | 6 +- internal/tui/components/core/layout/layout.go | 3 +- internal/tui/components/core/layout/split.go | 10 +-- internal/tui/components/core/list/keys.go | 20 ------ internal/tui/components/core/status/keys.go | 55 --------------- internal/tui/components/core/status/status.go | 25 +++++-- internal/tui/keys.go | 62 ++++++++++++++--- internal/tui/page/chat/chat.go | 66 +++++++++++++----- internal/tui/page/chat/keys.go | 35 +++------- internal/tui/page/logs/logs.go | 4 +- internal/tui/tui.go | 67 ++++++++++++------- todos.md | 23 +++++-- 15 files changed, 229 insertions(+), 205 deletions(-) delete mode 100644 internal/tui/components/core/status/keys.go diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index bd449123a886d5c88a5834e2dbe73a855451babd..d0c83857e5ea95ee582371d14c9ea1724669c60d 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/app" "github.com/charmbracelet/crush/internal/llm/agent" @@ -11,8 +12,8 @@ import ( "github.com/charmbracelet/crush/internal/pubsub" "github.com/charmbracelet/crush/internal/session" "github.com/charmbracelet/crush/internal/tui/components/chat/messages" - "github.com/charmbracelet/crush/internal/tui/components/core/list" "github.com/charmbracelet/crush/internal/tui/components/core/layout" + "github.com/charmbracelet/crush/internal/tui/components/core/list" "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" ) @@ -49,21 +50,23 @@ type messageListCmp struct { previousSelected int // Last selected item index for restoring focus lastUserMessageTime int64 + defaultListKeyMap list.KeyMap } // NewMessagesListCmp creates a new message list component with custom keybindings // and reverse ordering (newest messages at bottom). func NewMessagesListCmp(app *app.App) MessageListCmp { - defaultKeymaps := list.DefaultKeyMap() + defaultListKeyMap := list.DefaultKeyMap() listCmp := list.New( list.WithGapSize(1), list.WithReverse(true), - list.WithKeyMap(defaultKeymaps), + list.WithKeyMap(defaultListKeyMap), ) return &messageListCmp{ - app: app, - listCmp: listCmp, - previousSelected: list.NoSelection, + app: app, + listCmp: listCmp, + previousSelected: list.NoSelection, + defaultListKeyMap: defaultListKeyMap, } } @@ -495,3 +498,8 @@ func (m *messageListCmp) Focus() tea.Cmd { func (m *messageListCmp) IsFocused() bool { return m.listCmp.IsFocused() } + +func (m *messageListCmp) Bindings() []key.Binding { + bindings := layout.KeyMapToSlice(m.defaultListKeyMap) + return bindings +} diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index e47057f7ab7c2890cc3d97b3b3e94d567e7994ef..2f7e02b676512a226f8da5f25329a937c13c64dc 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -18,6 +18,7 @@ import ( "github.com/charmbracelet/crush/internal/session" "github.com/charmbracelet/crush/internal/tui/components/chat" "github.com/charmbracelet/crush/internal/tui/components/completions" + "github.com/charmbracelet/crush/internal/tui/components/core/layout" "github.com/charmbracelet/crush/internal/tui/components/dialogs" "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker" "github.com/charmbracelet/crush/internal/tui/components/dialogs/quit" @@ -248,7 +249,7 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } // Hanlde Enter key - if m.textarea.Focused() && key.Matches(msg, m.keyMap.Send) { + if m.textarea.Focused() && key.Matches(msg, m.keyMap.SendMessage) { value := m.textarea.Value() if len(value) > 0 && value[len(value)-1] == '\\' { // If the last character is a backslash, remove it and add a newline @@ -370,6 +371,10 @@ func (c *editorCmp) IsFocused() bool { return c.textarea.Focused() } +func (c *editorCmp) Bindings() []key.Binding { + return layout.KeyMapToSlice(c.keyMap) +} + func NewEditorCmp(app *app.App) util.Model { t := styles.CurrentTheme() ta := textarea.New() diff --git a/internal/tui/components/chat/editor/keys.go b/internal/tui/components/chat/editor/keys.go index 51c521244ec8e060333ff3e89328f73252ce5369..d384f1fdcad6977b984e51b11da01e8c46140ef5 100644 --- a/internal/tui/components/chat/editor/keys.go +++ b/internal/tui/components/chat/editor/keys.go @@ -2,17 +2,21 @@ package editor import ( "github.com/charmbracelet/bubbles/v2/key" - "github.com/charmbracelet/crush/internal/tui/components/core/layout" ) type EditorKeyMap struct { - Send key.Binding - OpenEditor key.Binding + AddFile key.Binding + SendMessage key.Binding + OpenEditor key.Binding } func DefaultEditorKeyMap() EditorKeyMap { return EditorKeyMap{ - Send: key.NewBinding( + AddFile: key.NewBinding( + key.WithKeys("/"), + key.WithHelp("/", "add file"), + ), + SendMessage: key.NewBinding( key.WithKeys("enter"), key.WithHelp("enter", "send"), ), @@ -23,25 +27,6 @@ func DefaultEditorKeyMap() EditorKeyMap { } } -// FullHelp implements help.KeyMap. -func (k EditorKeyMap) FullHelp() [][]key.Binding { - m := [][]key.Binding{} - slice := layout.KeyMapToSlice(k) - for i := 0; i < len(slice); i += 4 { - end := min(i+4, len(slice)) - m = append(m, slice[i:end]) - } - return m -} - -// ShortHelp implements help.KeyMap. -func (k EditorKeyMap) ShortHelp() []key.Binding { - return []key.Binding{ - k.Send, - k.OpenEditor, - } -} - // TODO: update this to use the new keymap concepts var AttachmentsKeyMaps = DeleteAttachmentKeyMaps{ AttachmentDeleteMode: key.NewBinding( diff --git a/internal/tui/components/core/layout/container.go b/internal/tui/components/core/layout/container.go index b0444130140d984051d4def0a17ea43f196050dd..02ae5c68d5aa885f56fb598127b358702a41e4c0 100644 --- a/internal/tui/components/core/layout/container.go +++ b/internal/tui/components/core/layout/container.go @@ -1,7 +1,7 @@ package layout import ( - "github.com/charmbracelet/bubbles/v2/help" + "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/tui/styles" "github.com/charmbracelet/crush/internal/tui/util" @@ -157,9 +157,9 @@ func (c *container) SetPosition(x, y int) tea.Cmd { return nil } -func (c *container) Help() help.KeyMap { +func (c *container) Bindings() []key.Binding { if b, ok := c.content.(Help); ok { - return b.Help() + return b.Bindings() } return nil } diff --git a/internal/tui/components/core/layout/layout.go b/internal/tui/components/core/layout/layout.go index ae43c51bf685134f859bf99ac9cef3d6328cb1e1..6c7ce4e11112423c0f3ab330e81cae8fb24e9908 100644 --- a/internal/tui/components/core/layout/layout.go +++ b/internal/tui/components/core/layout/layout.go @@ -3,7 +3,6 @@ package layout import ( "reflect" - "github.com/charmbracelet/bubbles/v2/help" "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" ) @@ -20,7 +19,7 @@ type Sizeable interface { } type Help interface { - Help() help.KeyMap + Bindings() []key.Binding } type Positionable interface { diff --git a/internal/tui/components/core/layout/split.go b/internal/tui/components/core/layout/split.go index d5c66ae0616d6ee4039e6e9e0933242d0c6daa28..9af08f48d47c4d3a1e2283fabf9d1634b291da35 100644 --- a/internal/tui/components/core/layout/split.go +++ b/internal/tui/components/core/layout/split.go @@ -1,7 +1,7 @@ package layout import ( - "github.com/charmbracelet/bubbles/v2/help" + "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/tui/styles" "github.com/charmbracelet/crush/internal/tui/util" @@ -269,20 +269,20 @@ func (s *splitPaneLayout) ClearBottomPanel() tea.Cmd { return nil } -func (s *splitPaneLayout) Help() help.KeyMap { +func (s *splitPaneLayout) Bindings() []key.Binding { if s.leftPanel != nil { if b, ok := s.leftPanel.(Help); ok && s.leftPanel.IsFocused() { - return b.Help() + return b.Bindings() } } if s.rightPanel != nil { if b, ok := s.rightPanel.(Help); ok && s.rightPanel.IsFocused() { - return b.Help() + return b.Bindings() } } if s.bottomPanel != nil { if b, ok := s.bottomPanel.(Help); ok && s.bottomPanel.IsFocused() { - return b.Help() + return b.Bindings() } } return nil diff --git a/internal/tui/components/core/list/keys.go b/internal/tui/components/core/list/keys.go index 92201cebdcc503ea621c75e851381c8ffad3a6ad..2bc5cc737b8191c36062b1c676cfd6e368aa576d 100644 --- a/internal/tui/components/core/list/keys.go +++ b/internal/tui/components/core/list/keys.go @@ -2,7 +2,6 @@ package list import ( "github.com/charmbracelet/bubbles/v2/key" - "github.com/charmbracelet/crush/internal/tui/components/core/layout" ) type KeyMap struct { @@ -52,22 +51,3 @@ func DefaultKeyMap() KeyMap { ), } } - -// FullHelp implements help.KeyMap. -func (k KeyMap) FullHelp() [][]key.Binding { - m := [][]key.Binding{} - slice := layout.KeyMapToSlice(k) - for i := 0; i < len(slice); i += 4 { - end := min(i+4, len(slice)) - m = append(m, slice[i:end]) - } - return m -} - -// ShortHelp implements help.KeyMap. -func (k KeyMap) ShortHelp() []key.Binding { - return []key.Binding{ - k.Up, - k.Down, - } -} diff --git a/internal/tui/components/core/status/keys.go b/internal/tui/components/core/status/keys.go deleted file mode 100644 index b16d035bfd0154ff0e8a82af87a67e2d2bd58053..0000000000000000000000000000000000000000 --- a/internal/tui/components/core/status/keys.go +++ /dev/null @@ -1,55 +0,0 @@ -package status - -import ( - "github.com/charmbracelet/bubbles/v2/key" - "github.com/charmbracelet/crush/internal/tui/components/core/layout" -) - -type KeyMap struct { - Tab, - Commands, - Sessions, - Help key.Binding -} - -func DefaultKeyMap(tabHelp string) KeyMap { - return KeyMap{ - Tab: key.NewBinding( - key.WithKeys("tab"), - key.WithHelp("tab", tabHelp), - ), - Commands: key.NewBinding( - key.WithKeys("ctrl+p"), - key.WithHelp("ctrl+p", "commands"), - ), - Sessions: key.NewBinding( - key.WithKeys("ctrl+s"), - key.WithHelp("ctrl+s", "sessions"), - ), - Help: key.NewBinding( - key.WithKeys("ctrl+?", "ctrl+_", "ctrl+/"), - key.WithHelp("ctrl+?", "more"), - ), - } -} - -// FullHelp implements help.KeyMap. -func (k KeyMap) FullHelp() [][]key.Binding { - m := [][]key.Binding{} - slice := layout.KeyMapToSlice(k) - for i := 0; i < len(slice); i += 4 { - end := min(i+4, len(slice)) - m = append(m, slice[i:end]) - } - return m -} - -// ShortHelp implements help.KeyMap. -func (k KeyMap) ShortHelp() []key.Binding { - return []key.Binding{ - k.Tab, - k.Commands, - k.Sessions, - k.Help, - } -} diff --git a/internal/tui/components/core/status/status.go b/internal/tui/components/core/status/status.go index ef5ebef108e9252aefa353fddefdde3538a28497..7b91c186f7ab9e572685de3e346204873d8cede2 100644 --- a/internal/tui/components/core/status/status.go +++ b/internal/tui/components/core/status/status.go @@ -14,6 +14,8 @@ import ( type StatusCmp interface { util.Model + ToggleFullHelp() + SetKeyMap(keyMap help.KeyMap) } type statusCmp struct { @@ -22,23 +24,25 @@ type statusCmp struct { messageTTL time.Duration session session.Session help help.Model + keyMap help.KeyMap } // clearMessageCmd is a command that clears status messages after a timeout -func (m statusCmp) clearMessageCmd(ttl time.Duration) tea.Cmd { +func (m *statusCmp) clearMessageCmd(ttl time.Duration) tea.Cmd { return tea.Tick(ttl, func(time.Time) tea.Msg { return util.ClearStatusMsg{} }) } -func (m statusCmp) Init() tea.Cmd { +func (m *statusCmp) Init() tea.Cmd { return nil } -func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.width = msg.Width + m.help.Width = msg.Width - 2 return m, nil // Handle status info @@ -86,9 +90,9 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m statusCmp) View() tea.View { +func (m *statusCmp) View() tea.View { t := styles.CurrentTheme() - status := t.S().Base.Padding(0, 1, 1, 1).Render(m.help.View(DefaultKeyMap("focus chat"))) + status := t.S().Base.Padding(0, 1, 1, 1).Render(m.help.View(m.keyMap)) if m.info.Msg != "" { switch m.info.Type { case util.InfoTypeError: @@ -102,12 +106,21 @@ func (m statusCmp) View() tea.View { return tea.NewView(status) } -func NewStatusCmp() StatusCmp { +func (m *statusCmp) ToggleFullHelp() { + m.help.ShowAll = !m.help.ShowAll +} + +func (m *statusCmp) SetKeyMap(keyMap help.KeyMap) { + m.keyMap = keyMap +} + +func NewStatusCmp(keyMap help.KeyMap) StatusCmp { t := styles.CurrentTheme() help := help.New() help.Styles = t.S().Help return &statusCmp{ messageTTL: 10 * time.Second, help: help, + keyMap: keyMap, } } diff --git a/internal/tui/keys.go b/internal/tui/keys.go index 2c1d0d038b220d3bc97e78b105caea15f7699149..dda3ad4dba02192626adf74540d2c62aad44a5a1 100644 --- a/internal/tui/keys.go +++ b/internal/tui/keys.go @@ -2,7 +2,6 @@ package tui import ( "github.com/charmbracelet/bubbles/v2/key" - "github.com/charmbracelet/crush/internal/tui/components/core/layout" ) type KeyMap struct { @@ -11,6 +10,8 @@ type KeyMap struct { Help key.Binding Commands key.Binding Sessions key.Binding + + pageBindings []key.Binding } func DefaultKeyMap() KeyMap { @@ -23,10 +24,9 @@ func DefaultKeyMap() KeyMap { key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "quit"), ), - Help: key.NewBinding( - key.WithKeys("ctrl+_"), - key.WithHelp("ctrl+?", "toggle help"), + key.WithKeys("ctrl+?", "ctrl+_", "ctrl+/"), + key.WithHelp("ctrl+?", "more"), ), Commands: key.NewBinding( key.WithKeys("ctrl+p"), @@ -42,15 +42,59 @@ func DefaultKeyMap() KeyMap { // FullHelp implements help.KeyMap. func (k KeyMap) FullHelp() [][]key.Binding { m := [][]key.Binding{} - slice := layout.KeyMapToSlice(k) - for i := 0; i < len(slice); i += 4 { - end := min(i+4, len(slice)) - m = append(m, slice[i:end]) + slice := []key.Binding{ + k.Commands, + k.Sessions, + k.Quit, + k.Help, + k.Logs, + } + slice = k.prependEscAndTab(slice) + slice = append(slice, k.pageBindings...) + // remove duplicates + seen := make(map[string]bool) + cleaned := []key.Binding{} + for _, b := range slice { + if !seen[b.Help().Key] { + seen[b.Help().Key] = true + cleaned = append(cleaned, b) + } + } + + for i := 0; i < len(cleaned); i += 2 { + end := min(i+2, len(cleaned)) + m = append(m, cleaned[i:end]) } return m } +func (k KeyMap) prependEscAndTab(bindings []key.Binding) []key.Binding { + var cancel key.Binding + var tab key.Binding + for _, b := range k.pageBindings { + if b.Help().Key == "esc" { + cancel = b + } + if b.Help().Key == "tab" { + tab = b + } + } + if tab.Help().Key != "" { + bindings = append([]key.Binding{tab}, bindings...) + } + if cancel.Help().Key != "" { + bindings = append([]key.Binding{cancel}, bindings...) + } + return bindings +} + // ShortHelp implements help.KeyMap. func (k KeyMap) ShortHelp() []key.Binding { - return []key.Binding{} + bindings := []key.Binding{ + k.Commands, + k.Sessions, + k.Quit, + k.Help, + } + return k.prependEscAndTab(bindings) } diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index ff769f9fee40545e003167d91ff29fcbee83b906..fa374abb0bee72330d840b858b5219d82554b5ed 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -13,32 +13,37 @@ import ( "github.com/charmbracelet/crush/internal/tui/components/chat" "github.com/charmbracelet/crush/internal/tui/components/chat/editor" "github.com/charmbracelet/crush/internal/tui/components/chat/sidebar" - "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands" "github.com/charmbracelet/crush/internal/tui/components/core/layout" + "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands" "github.com/charmbracelet/crush/internal/tui/page" "github.com/charmbracelet/crush/internal/tui/util" ) -var ChatPage page.PageID = "chat" - -type ChatFocusedMsg struct { - Focused bool // True if the chat input is focused, false otherwise -} +var ChatPageID page.PageID = "chat" type ( OpenFilePickerMsg struct{} - chatPage struct { - app *app.App + ChatFocusedMsg struct { + Focused bool // True if the chat input is focused, false otherwise + } +) - layout layout.SplitPaneLayout +type ChatPage interface { + util.Model + layout.Help +} - session session.Session +type chatPage struct { + app *app.App - keyMap KeyMap + layout layout.SplitPaneLayout - chatFocused bool - } -) + session session.Session + + keyMap KeyMap + + chatFocused bool +} func (p *chatPage) Init() tea.Cmd { cmd := p.layout.Init() @@ -87,7 +92,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { util.CmdHandler(chat.SessionClearedMsg{}), ) - case key.Matches(msg, p.keyMap.FilePicker): + case key.Matches(msg, p.keyMap.AddAttachment): cfg := config.Get() agentCfg := cfg.Agents[config.AgentCoder] selectedModelID := agentCfg.Model @@ -173,7 +178,36 @@ func (p *chatPage) View() tea.View { return p.layout.View() } -func NewChatPage(app *app.App) util.Model { +func (p *chatPage) Bindings() []key.Binding { + bindings := []key.Binding{ + p.keyMap.NewSession, + p.keyMap.AddAttachment, + } + if p.app.CoderAgent.IsBusy() { + bindings = append([]key.Binding{p.keyMap.Cancel}, bindings...) + } + + if p.chatFocused { + bindings = append([]key.Binding{ + key.NewBinding( + key.WithKeys("tab"), + key.WithHelp("tab", "focus editor"), + ), + }, bindings...) + } else { + bindings = append([]key.Binding{ + key.NewBinding( + key.WithKeys("tab"), + key.WithHelp("tab", "focus chat"), + ), + }, bindings...) + } + + bindings = append(bindings, p.layout.Bindings()...) + return bindings +} + +func NewChatPage(app *app.App) ChatPage { sidebarContainer := layout.NewContainer( sidebar.NewSidebarCmp(), layout.WithPadding(1, 1, 1, 1), diff --git a/internal/tui/page/chat/keys.go b/internal/tui/page/chat/keys.go index 9858a9d5821704334cac75dcdbb1d4ca228234bb..87167ee2c47162bb9c7c4111d100bd80283e5bbe 100644 --- a/internal/tui/page/chat/keys.go +++ b/internal/tui/page/chat/keys.go @@ -2,14 +2,13 @@ package chat import ( "github.com/charmbracelet/bubbles/v2/key" - "github.com/charmbracelet/crush/internal/tui/components/core/layout" ) type KeyMap struct { - NewSession key.Binding - FilePicker key.Binding - Cancel key.Binding - Tab key.Binding + NewSession key.Binding + AddAttachment key.Binding + Cancel key.Binding + Tab key.Binding } func DefaultKeyMap() KeyMap { @@ -18,6 +17,10 @@ func DefaultKeyMap() KeyMap { key.WithKeys("ctrl+n"), key.WithHelp("ctrl+n", "new session"), ), + AddAttachment: key.NewBinding( + key.WithKeys("ctrl+f"), + key.WithHelp("ctrl+f", "add attachment"), + ), Cancel: key.NewBinding( key.WithKeys("esc"), key.WithHelp("esc", "cancel"), @@ -26,27 +29,5 @@ func DefaultKeyMap() KeyMap { key.WithKeys("tab"), key.WithHelp("tab", "change focus"), ), - FilePicker: key.NewBinding( - key.WithKeys("ctrl+f"), - key.WithHelp("ctrl+f", "select files to upload"), - ), - } -} - -// FullHelp implements help.KeyMap. -func (k KeyMap) FullHelp() [][]key.Binding { - m := [][]key.Binding{} - slice := layout.KeyMapToSlice(k) - for i := 0; i < len(slice); i += 4 { - end := min(i+4, len(slice)) - m = append(m, slice[i:end]) - } - return m -} - -// ShortHelp implements help.KeyMap. -func (k KeyMap) ShortHelp() []key.Binding { - return []key.Binding{ - k.Tab, } } diff --git a/internal/tui/page/logs/logs.go b/internal/tui/page/logs/logs.go index 39f7137178bb9fe0a58d798ac32434402c0d133c..e8b1077d80094ee5974debc5c09f1f82562e334c 100644 --- a/internal/tui/page/logs/logs.go +++ b/internal/tui/page/logs/logs.go @@ -4,8 +4,8 @@ import ( "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/tui/components/core" - logsComponents "github.com/charmbracelet/crush/internal/tui/components/logs" "github.com/charmbracelet/crush/internal/tui/components/core/layout" + logsComponents "github.com/charmbracelet/crush/internal/tui/components/logs" "github.com/charmbracelet/crush/internal/tui/page" "github.com/charmbracelet/crush/internal/tui/page/chat" "github.com/charmbracelet/crush/internal/tui/styles" @@ -37,7 +37,7 @@ func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch { case key.Matches(msg, p.keyMap.Back): - return p, util.CmdHandler(page.PageChangeMsg{ID: chat.ChatPage}) + return p, util.CmdHandler(page.PageChangeMsg{ID: chat.ChatPageID}) } } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index a4c2e48f58e1cfc34b1ce12788885bd1731894d8..50e221a1520c446169fce544b494285f03a1e778 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -34,24 +34,26 @@ import ( // appModel represents the main application model that manages pages, dialogs, and UI state. type appModel struct { - width, height int - keyMap KeyMap + wWidth, wHeight int // Window dimensions + width, height int + keyMap KeyMap currentPage page.PageID previousPage page.PageID pages map[page.PageID]util.Model loadedPages map[page.PageID]bool - status status.StatusCmp + // Status + status status.StatusCmp + showingFullHelp bool app *app.App dialog dialogs.DialogCmp completions completions.Completions - // Session + // Chat Page Specific selectedSessionID string // The ID of the currently selected session - fullHelp bool // Whether to show full help text } // Init initializes the application model and returns initial commands. @@ -99,7 +101,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { ) return a, nil case tea.WindowSizeMsg: - return a, a.handleWindowResize(msg) + return a, a.handleWindowResize(msg.Width, msg.Height) // Completions messages case completions.OpenCompletionsMsg, completions.FilterCompletionsMsg, completions.CloseCompletionsMsg: @@ -244,23 +246,29 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // handleWindowResize processes window resize events and updates all components. -func (a *appModel) handleWindowResize(msg tea.WindowSizeMsg) tea.Cmd { +func (a *appModel) handleWindowResize(width, height int) tea.Cmd { var cmds []tea.Cmd - msg.Height -= 2 // Make space for the status bar - a.width, a.height = msg.Width, msg.Height - + a.wWidth, a.wHeight = width, height + if a.showingFullHelp { + height -= 3 + } else { + height -= 2 + } + a.width, a.height = width, height // Update status bar - s, cmd := a.status.Update(msg) + s, cmd := a.status.Update(tea.WindowSizeMsg{Width: width, Height: height}) a.status = s.(status.StatusCmp) cmds = append(cmds, cmd) // Update the current page - updated, cmd := a.pages[a.currentPage].Update(msg) - a.pages[a.currentPage] = updated.(util.Model) - cmds = append(cmds, cmd) + for p, page := range a.pages { + updated, pageCmd := page.Update(tea.WindowSizeMsg{Width: width, Height: height}) + a.pages[p] = updated.(util.Model) + cmds = append(cmds, pageCmd) + } // Update the dialogs - dialog, cmd := a.dialog.Update(msg) + dialog, cmd := a.dialog.Update(tea.WindowSizeMsg{Width: width, Height: height}) a.dialog = dialog.(dialogs.DialogCmp) cmds = append(cmds, cmd) @@ -288,6 +296,11 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { u, cmd := a.completions.Update(msg) a.completions = u.(completions.Completions) return cmd + // help + case key.Matches(msg, a.keyMap.Help): + a.status.ToggleFullHelp() + a.showingFullHelp = !a.showingFullHelp + return a.handleWindowResize(a.wWidth, a.wHeight) // dialogs case key.Matches(msg, a.keyMap.Quit): if a.dialog.ActiveDialogID() == quit.QuitDialogID { @@ -345,7 +358,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { // moveToPage handles navigation between different pages in the application. 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 + // TODO: maybe remove this : For now we don't move to any page if the agent is busy return util.ReportWarn("Agent is busy, please wait...") } @@ -367,7 +380,12 @@ func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd { // View renders the complete application interface including pages, dialogs, and overlays. func (a *appModel) View() tea.View { - pageView := a.pages[a.currentPage].View() + page := a.pages[a.currentPage] + if withHelp, ok := page.(layout.Help); ok { + a.keyMap.pageBindings = withHelp.Bindings() + } + a.status.SetKeyMap(a.keyMap) + pageView := page.View() components := []string{ pageView.String(), } @@ -412,17 +430,20 @@ func (a *appModel) View() tea.View { // New creates and initializes a new TUI application model. func New(app *app.App) tea.Model { - startPage := chat.ChatPage + chatPage := chat.NewChatPage(app) + keyMap := DefaultKeyMap() + keyMap.pageBindings = chatPage.Bindings() + model := &appModel{ - currentPage: startPage, + currentPage: chat.ChatPageID, app: app, - status: status.NewStatusCmp(), + status: status.NewStatusCmp(keyMap), loadedPages: make(map[page.PageID]bool), - keyMap: DefaultKeyMap(), + keyMap: keyMap, pages: map[page.PageID]util.Model{ - chat.ChatPage: chat.NewChatPage(app), - logs.LogsPage: logs.NewLogsPage(), + chat.ChatPageID: chatPage, + logs.LogsPage: logs.NewLogsPage(), }, dialog: dialogs.NewDialogCmp(), diff --git a/todos.md b/todos.md index 90dadc5c7a469c067a246fc4c926d268af62afd5..85ce7a39c019fe86508888c1254049091ff87e2c 100644 --- a/todos.md +++ b/todos.md @@ -1,11 +1,18 @@ ## TODOs before release -- [~] Implement help - - [ ] Show full help - - [ ] Make help dependent on the focused pane and page +- [x] Implement help + - [x] Show full help + - [x] Make help dependent on the focused pane and page +- [ ] Implement current model in the sidebar +- [ ] Implement changed files - [ ] Events when tool error -- [ ] Fix issue with numbers (padding) -- [ ] Fancy Spinner +- [ ] Support bash commands +- [ ] Editor attachments fixes + - [ ] Reimplement removing attachments +- [ ] Fix the logs view + - [ ] Review the implementation + - [ ] The page lags + - [ ] Make the logs long lived ? - [ ] Add all possible actions to the commands - [ ] Parallel tool calls and permissions - [ ] Run the tools in parallel and add results in parallel @@ -14,14 +21,16 @@ - [ ] Weird behavior sometimes the message does not update - [ ] Message length (I saw the message go beyond the correct length when there are errors) - [ ] Address UX issues -- [ ] Implement current model in the sidebar -- [ ] Implement changed files + - [ ] Fix issue with numbers (padding) view tool - [ ] Implement responsive mode - [ ] Revisit the core list component - [ ] This component has become super complex we might need to fix this. +- [ ] Investigate ways to make the spinner less CPU intensive - [ ] General cleanup and documentation - [ ] Update the readme ## Maybe - [ ] Revisit the provider/model/configs +- [ ] Implement correct persistent shell +- [ ] Store file read/write time somewhere so that the we can make sure that even if we restart we do not need to re-read the same file