on main help sign in
feat: implement show all help
commit967bf95e2f4bd892db3cce243ec4cb5073b40004 authorKujtim Hoxha <kujtimii.h@gmail.com> dateMon 16 Jun 2025, 13:37 (1 year ago) parentd957f5d
15 files changed+229−205
internal/tui/components/chat/chat.go+14−6 internal/tui/components/chat/editor/editor.go+6−1 internal/tui/components/chat/editor/keys.go+8−23 internal/tui/components/core/layout/container.go+3−3 internal/tui/components/core/layout/layout.go+1−2 internal/tui/components/core/layout/split.go+5−5 internal/tui/components/core/list/keys.go−20 internal/tui/components/core/status/keys.go−55 internal/tui/components/core/status/status.go+19−6 internal/tui/keys.go+53−9 internal/tui/page/chat/chat.go+50−16 internal/tui/page/chat/keys.go+8−27 internal/tui/page/logs/logs.go+2−2 internal/tui/tui.go+44−23 todos.md+16−7
internal/tui/components/chat/chat.go+14−6
@@ -4,6 +4,7 @@ import (
44 "context"
55 "time"
66
7+ "github.com/charmbracelet/bubbles/v2/key"
78 tea "github.com/charmbracelet/bubbletea/v2"
89 "github.com/charmbracelet/crush/internal/app"
910 "github.com/charmbracelet/crush/internal/llm/agent"
@@ -11,8 +12,8 @@ import (
1112 "github.com/charmbracelet/crush/internal/pubsub"
1213 "github.com/charmbracelet/crush/internal/session"
1314 "github.com/charmbracelet/crush/internal/tui/components/chat/messages"
14− "github.com/charmbracelet/crush/internal/tui/components/core/list"
1515 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
16+ "github.com/charmbracelet/crush/internal/tui/components/core/list"
1617 "github.com/charmbracelet/crush/internal/tui/util"
1718 "github.com/charmbracelet/lipgloss/v2"
1819)
@@ -49,21 +50,23 @@ type messageListCmp struct {
4950 previousSelected int // Last selected item index for restoring focus
5051
5152 lastUserMessageTime int64
53+ defaultListKeyMap list.KeyMap
5254}
5355
5456// NewMessagesListCmp creates a new message list component with custom keybindings
5557// and reverse ordering (newest messages at bottom).
5658func NewMessagesListCmp(app *app.App) MessageListCmp {
57− defaultKeymaps := list.DefaultKeyMap()
59+ defaultListKeyMap := list.DefaultKeyMap()
5860 listCmp := list.New(
5961 list.WithGapSize(1),
6062 list.WithReverse(true),
61− list.WithKeyMap(defaultKeymaps),
63+ list.WithKeyMap(defaultListKeyMap),
6264 )
6365 return &messageListCmp{
64− app: app,
65− listCmp: listCmp,
66− previousSelected: list.NoSelection,
66+ app: app,
67+ listCmp: listCmp,
68+ previousSelected: list.NoSelection,
69+ defaultListKeyMap: defaultListKeyMap,
6770 }
6871}
6972
@@ -495,3 +498,8 @@ func (m *messageListCmp) Focus() tea.Cmd {
495498func (m *messageListCmp) IsFocused() bool {
496499 return m.listCmp.IsFocused()
497500}
501+
502+func (m *messageListCmp) Bindings() []key.Binding {
503+ bindings := layout.KeyMapToSlice(m.defaultListKeyMap)
504+ return bindings
505+}
internal/tui/components/chat/editor/editor.go+6−1
@@ -18,6 +18,7 @@ import (
1818 "github.com/charmbracelet/crush/internal/session"
1919 "github.com/charmbracelet/crush/internal/tui/components/chat"
2020 "github.com/charmbracelet/crush/internal/tui/components/completions"
21+ "github.com/charmbracelet/crush/internal/tui/components/core/layout"
2122 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
2223 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
2324 "github.com/charmbracelet/crush/internal/tui/components/dialogs/quit"
@@ -248,7 +249,7 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
248249 return m, nil
249250 }
250251 // Hanlde Enter key
251− if m.textarea.Focused() && key.Matches(msg, m.keyMap.Send) {
252+ if m.textarea.Focused() && key.Matches(msg, m.keyMap.SendMessage) {
252253 value := m.textarea.Value()
253254 if len(value) > 0 && value[len(value)-1] == '\\' {
254255 // If the last character is a backslash, remove it and add a newline
@@ -370,6 +371,10 @@ func (c *editorCmp) IsFocused() bool {
370371 return c.textarea.Focused()
371372}
372373
374+func (c *editorCmp) Bindings() []key.Binding {
375+ return layout.KeyMapToSlice(c.keyMap)
376+}
377+
373378func NewEditorCmp(app *app.App) util.Model {
374379 t := styles.CurrentTheme()
375380 ta := textarea.New()
internal/tui/components/chat/editor/keys.go+8−23
@@ -2,17 +2,21 @@ package editor
22
33import (
44 "github.com/charmbracelet/bubbles/v2/key"
5− "github.com/charmbracelet/crush/internal/tui/components/core/layout"
65)
76
87type EditorKeyMap struct {
9− Send key.Binding
10− OpenEditor key.Binding
8+ AddFile key.Binding
9+ SendMessage key.Binding
10+ OpenEditor key.Binding
1111}
1212
1313func DefaultEditorKeyMap() EditorKeyMap {
1414 return EditorKeyMap{
15− Send: key.NewBinding(
15+ AddFile: key.NewBinding(
16+ key.WithKeys("/"),
17+ key.WithHelp("/", "add file"),
18+ ),
19+ SendMessage: key.NewBinding(
1620 key.WithKeys("enter"),
1721 key.WithHelp("enter", "send"),
1822 ),
@@ -23,25 +27,6 @@ func DefaultEditorKeyMap() EditorKeyMap {
2327 }
2428}
2529
26−// FullHelp implements help.KeyMap.
27−func (k EditorKeyMap) FullHelp() [][]key.Binding {
28− m := [][]key.Binding{}
29− slice := layout.KeyMapToSlice(k)
30− for i := 0; i < len(slice); i += 4 {
31− end := min(i+4, len(slice))
32− m = append(m, slice[i:end])
33− }
34− return m
35−}
36−
37−// ShortHelp implements help.KeyMap.
38−func (k EditorKeyMap) ShortHelp() []key.Binding {
39− return []key.Binding{
40− k.Send,
41− k.OpenEditor,
42− }
43−}
44−
4530// TODO: update this to use the new keymap concepts
4631var AttachmentsKeyMaps = DeleteAttachmentKeyMaps{
4732 AttachmentDeleteMode: key.NewBinding(
internal/tui/components/core/layout/container.go+3−3
@@ -1,7 +1,7 @@
11package layout
22
33import (
4− "github.com/charmbracelet/bubbles/v2/help"
4+ "github.com/charmbracelet/bubbles/v2/key"
55 tea "github.com/charmbracelet/bubbletea/v2"
66 "github.com/charmbracelet/crush/internal/tui/styles"
77 "github.com/charmbracelet/crush/internal/tui/util"
@@ -157,9 +157,9 @@ func (c *container) SetPosition(x, y int) tea.Cmd {
157157 return nil
158158}
159159
160−func (c *container) Help() help.KeyMap {
160+func (c *container) Bindings() []key.Binding {
161161 if b, ok := c.content.(Help); ok {
162− return b.Help()
162+ return b.Bindings()
163163 }
164164 return nil
165165}
internal/tui/components/core/layout/layout.go+1−2
@@ -3,7 +3,6 @@ package layout
33import (
44 "reflect"
55
6− "github.com/charmbracelet/bubbles/v2/help"
76 "github.com/charmbracelet/bubbles/v2/key"
87 tea "github.com/charmbracelet/bubbletea/v2"
98)
@@ -20,7 +19,7 @@ type Sizeable interface {
2019}
2120
2221type Help interface {
23− Help() help.KeyMap
22+ Bindings() []key.Binding
2423}
2524
2625type Positionable interface {
internal/tui/components/core/layout/split.go+5−5
@@ -1,7 +1,7 @@
11package layout
22
33import (
4− "github.com/charmbracelet/bubbles/v2/help"
4+ "github.com/charmbracelet/bubbles/v2/key"
55 tea "github.com/charmbracelet/bubbletea/v2"
66 "github.com/charmbracelet/crush/internal/tui/styles"
77 "github.com/charmbracelet/crush/internal/tui/util"
@@ -269,20 +269,20 @@ func (s *splitPaneLayout) ClearBottomPanel() tea.Cmd {
269269 return nil
270270}
271271
272−func (s *splitPaneLayout) Help() help.KeyMap {
272+func (s *splitPaneLayout) Bindings() []key.Binding {
273273 if s.leftPanel != nil {
274274 if b, ok := s.leftPanel.(Help); ok && s.leftPanel.IsFocused() {
275− return b.Help()
275+ return b.Bindings()
276276 }
277277 }
278278 if s.rightPanel != nil {
279279 if b, ok := s.rightPanel.(Help); ok && s.rightPanel.IsFocused() {
280− return b.Help()
280+ return b.Bindings()
281281 }
282282 }
283283 if s.bottomPanel != nil {
284284 if b, ok := s.bottomPanel.(Help); ok && s.bottomPanel.IsFocused() {
285− return b.Help()
285+ return b.Bindings()
286286 }
287287 }
288288 return nil
internal/tui/components/core/list/keys.go−20
@@ -2,7 +2,6 @@ package list
22
33import (
44 "github.com/charmbracelet/bubbles/v2/key"
5− "github.com/charmbracelet/crush/internal/tui/components/core/layout"
65)
76
87type KeyMap struct {
@@ -52,22 +51,3 @@ func DefaultKeyMap() KeyMap {
5251 ),
5352 }
5453}
55−
56−// FullHelp implements help.KeyMap.
57−func (k KeyMap) FullHelp() [][]key.Binding {
58− m := [][]key.Binding{}
59− slice := layout.KeyMapToSlice(k)
60− for i := 0; i < len(slice); i += 4 {
61− end := min(i+4, len(slice))
62− m = append(m, slice[i:end])
63− }
64− return m
65−}
66−
67−// ShortHelp implements help.KeyMap.
68−func (k KeyMap) ShortHelp() []key.Binding {
69− return []key.Binding{
70− k.Up,
71− k.Down,
72− }
73−}
internal/tui/components/core/status/keys.godeleted−55
@@ -1,55 +0,0 @@
1−package status
2−
3−import (
4− "github.com/charmbracelet/bubbles/v2/key"
5− "github.com/charmbracelet/crush/internal/tui/components/core/layout"
6−)
7−
8−type KeyMap struct {
9− Tab,
10− Commands,
11− Sessions,
12− Help key.Binding
13−}
14−
15−func DefaultKeyMap(tabHelp string) KeyMap {
16− return KeyMap{
17− Tab: key.NewBinding(
18− key.WithKeys("tab"),
19− key.WithHelp("tab", tabHelp),
20− ),
21− Commands: key.NewBinding(
22− key.WithKeys("ctrl+p"),
23− key.WithHelp("ctrl+p", "commands"),
24− ),
25− Sessions: key.NewBinding(
26− key.WithKeys("ctrl+s"),
27− key.WithHelp("ctrl+s", "sessions"),
28− ),
29− Help: key.NewBinding(
30− key.WithKeys("ctrl+?", "ctrl+_", "ctrl+/"),
31− key.WithHelp("ctrl+?", "more"),
32− ),
33− }
34−}
35−
36−// FullHelp implements help.KeyMap.
37−func (k KeyMap) FullHelp() [][]key.Binding {
38− m := [][]key.Binding{}
39− slice := layout.KeyMapToSlice(k)
40− for i := 0; i < len(slice); i += 4 {
41− end := min(i+4, len(slice))
42− m = append(m, slice[i:end])
43− }
44− return m
45−}
46−
47−// ShortHelp implements help.KeyMap.
48−func (k KeyMap) ShortHelp() []key.Binding {
49− return []key.Binding{
50− k.Tab,
51− k.Commands,
52− k.Sessions,
53− k.Help,
54− }
55−}
internal/tui/components/core/status/status.go+19−6
@@ -14,6 +14,8 @@ import (
1414
1515type StatusCmp interface {
1616 util.Model
17+ ToggleFullHelp()
18+ SetKeyMap(keyMap help.KeyMap)
1719}
1820
1921type statusCmp struct {
@@ -22,23 +24,25 @@ type statusCmp struct {
2224 messageTTL time.Duration
2325 session session.Session
2426 help help.Model
27+ keyMap help.KeyMap
2528}
2629
2730// clearMessageCmd is a command that clears status messages after a timeout
28−func (m statusCmp) clearMessageCmd(ttl time.Duration) tea.Cmd {
31+func (m *statusCmp) clearMessageCmd(ttl time.Duration) tea.Cmd {
2932 return tea.Tick(ttl, func(time.Time) tea.Msg {
3033 return util.ClearStatusMsg{}
3134 })
3235}
3336
34−func (m statusCmp) Init() tea.Cmd {
37+func (m *statusCmp) Init() tea.Cmd {
3538 return nil
3639}
3740
38−func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
41+func (m *statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
3942 switch msg := msg.(type) {
4043 case tea.WindowSizeMsg:
4144 m.width = msg.Width
45+ m.help.Width = msg.Width - 2
4246 return m, nil
4347
4448 // Handle status info
@@ -86,9 +90,9 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8690 return m, nil
8791}
8892
89−func (m statusCmp) View() tea.View {
93+func (m *statusCmp) View() tea.View {
9094 t := styles.CurrentTheme()
91− status := t.S().Base.Padding(0, 1, 1, 1).Render(m.help.View(DefaultKeyMap("focus chat")))
95+ status := t.S().Base.Padding(0, 1, 1, 1).Render(m.help.View(m.keyMap))
9296 if m.info.Msg != "" {
9397 switch m.info.Type {
9498 case util.InfoTypeError:
@@ -102,12 +106,21 @@ func (m statusCmp) View() tea.View {
102106 return tea.NewView(status)
103107}
104108
105−func NewStatusCmp() StatusCmp {
109+func (m *statusCmp) ToggleFullHelp() {
110+ m.help.ShowAll = !m.help.ShowAll
111+}
112+
113+func (m *statusCmp) SetKeyMap(keyMap help.KeyMap) {
114+ m.keyMap = keyMap
115+}
116+
117+func NewStatusCmp(keyMap help.KeyMap) StatusCmp {
106118 t := styles.CurrentTheme()
107119 help := help.New()
108120 help.Styles = t.S().Help
109121 return &statusCmp{
110122 messageTTL: 10 * time.Second,
111123 help: help,
124+ keyMap: keyMap,
112125 }
113126}
internal/tui/keys.go+53−9
@@ -2,7 +2,6 @@ package tui
22
33import (
44 "github.com/charmbracelet/bubbles/v2/key"
5− "github.com/charmbracelet/crush/internal/tui/components/core/layout"
65)
76
87type KeyMap struct {
@@ -11,6 +10,8 @@ type KeyMap struct {
1110 Help key.Binding
1211 Commands key.Binding
1312 Sessions key.Binding
13+
14+ pageBindings []key.Binding
1415}
1516
1617func DefaultKeyMap() KeyMap {
@@ -23,10 +24,9 @@ func DefaultKeyMap() KeyMap {
2324 key.WithKeys("ctrl+c"),
2425 key.WithHelp("ctrl+c", "quit"),
2526 ),
26−
2727 Help: key.NewBinding(
28− key.WithKeys("ctrl+_"),
29− key.WithHelp("ctrl+?", "toggle help"),
28+ key.WithKeys("ctrl+?", "ctrl+_", "ctrl+/"),
29+ key.WithHelp("ctrl+?", "more"),
3030 ),
3131 Commands: key.NewBinding(
3232 key.WithKeys("ctrl+p"),
@@ -42,15 +42,59 @@ func DefaultKeyMap() KeyMap {
4242// FullHelp implements help.KeyMap.
4343func (k KeyMap) FullHelp() [][]key.Binding {
4444 m := [][]key.Binding{}
45− slice := layout.KeyMapToSlice(k)
46− for i := 0; i < len(slice); i += 4 {
47− end := min(i+4, len(slice))
48− m = append(m, slice[i:end])
45+ slice := []key.Binding{
46+ k.Commands,
47+ k.Sessions,
48+ k.Quit,
49+ k.Help,
50+ k.Logs,
51+ }
52+ slice = k.prependEscAndTab(slice)
53+ slice = append(slice, k.pageBindings...)
54+ // remove duplicates
55+ seen := make(map[string]bool)
56+ cleaned := []key.Binding{}
57+ for _, b := range slice {
58+ if !seen[b.Help().Key] {
59+ seen[b.Help().Key] = true
60+ cleaned = append(cleaned, b)
61+ }
62+ }
63+
64+ for i := 0; i < len(cleaned); i += 2 {
65+ end := min(i+2, len(cleaned))
66+ m = append(m, cleaned[i:end])
4967 }
5068 return m
5169}
5270
71+func (k KeyMap) prependEscAndTab(bindings []key.Binding) []key.Binding {
72+ var cancel key.Binding
73+ var tab key.Binding
74+ for _, b := range k.pageBindings {
75+ if b.Help().Key == "esc" {
76+ cancel = b
77+ }
78+ if b.Help().Key == "tab" {
79+ tab = b
80+ }
81+ }
82+ if tab.Help().Key != "" {
83+ bindings = append([]key.Binding{tab}, bindings...)
84+ }
85+ if cancel.Help().Key != "" {
86+ bindings = append([]key.Binding{cancel}, bindings...)
87+ }
88+ return bindings
89+}
90+
5391// ShortHelp implements help.KeyMap.
5492func (k KeyMap) ShortHelp() []key.Binding {
55− return []key.Binding{}
93+ bindings := []key.Binding{
94+ k.Commands,
95+ k.Sessions,
96+ k.Quit,
97+ k.Help,
98+ }
99+ return k.prependEscAndTab(bindings)
56100}
internal/tui/page/chat/chat.go+50−16
@@ -13,32 +13,37 @@ import (
1313 "github.com/charmbracelet/crush/internal/tui/components/chat"
1414 "github.com/charmbracelet/crush/internal/tui/components/chat/editor"
1515 "github.com/charmbracelet/crush/internal/tui/components/chat/sidebar"
16− "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
1716 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
17+ "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
1818 "github.com/charmbracelet/crush/internal/tui/page"
1919 "github.com/charmbracelet/crush/internal/tui/util"
2020)
2121
22−var ChatPage page.PageID = "chat"
23−
24−type ChatFocusedMsg struct {
25− Focused bool // True if the chat input is focused, false otherwise
26−}
22+var ChatPageID page.PageID = "chat"
2723
2824type (
2925 OpenFilePickerMsg struct{}
30− chatPage struct {
31− app *app.App
26+ ChatFocusedMsg struct {
27+ Focused bool // True if the chat input is focused, false otherwise
28+ }
29+)
3230
33− layout layout.SplitPaneLayout
31+type ChatPage interface {
32+ util.Model
33+ layout.Help
34+}
3435
35− session session.Session
36+type chatPage struct {
37+ app *app.App
3638
37− keyMap KeyMap
39+ layout layout.SplitPaneLayout
3840
39− chatFocused bool
40− }
41−)
41+ session session.Session
42+
43+ keyMap KeyMap
44+
45+ chatFocused bool
46+}
4247
4348func (p *chatPage) Init() tea.Cmd {
4449 cmd := p.layout.Init()
@@ -87,7 +92,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8792 util.CmdHandler(chat.SessionClearedMsg{}),
8893 )
8994
90− case key.Matches(msg, p.keyMap.FilePicker):
95+ case key.Matches(msg, p.keyMap.AddAttachment):
9196 cfg := config.Get()
9297 agentCfg := cfg.Agents[config.AgentCoder]
9398 selectedModelID := agentCfg.Model
@@ -173,7 +178,36 @@ func (p *chatPage) View() tea.View {
173178 return p.layout.View()
174179}
175180
176−func NewChatPage(app *app.App) util.Model {
181+func (p *chatPage) Bindings() []key.Binding {
182+ bindings := []key.Binding{
183+ p.keyMap.NewSession,
184+ p.keyMap.AddAttachment,
185+ }
186+ if p.app.CoderAgent.IsBusy() {
187+ bindings = append([]key.Binding{p.keyMap.Cancel}, bindings...)
188+ }
189+
190+ if p.chatFocused {
191+ bindings = append([]key.Binding{
192+ key.NewBinding(
193+ key.WithKeys("tab"),
194+ key.WithHelp("tab", "focus editor"),
195+ ),
196+ }, bindings...)
197+ } else {
198+ bindings = append([]key.Binding{
199+ key.NewBinding(
200+ key.WithKeys("tab"),
201+ key.WithHelp("tab", "focus chat"),
202+ ),
203+ }, bindings...)
204+ }
205+
206+ bindings = append(bindings, p.layout.Bindings()...)
207+ return bindings
208+}
209+
210+func NewChatPage(app *app.App) ChatPage {
177211 sidebarContainer := layout.NewContainer(
178212 sidebar.NewSidebarCmp(),
179213 layout.WithPadding(1, 1, 1, 1),
internal/tui/page/chat/keys.go+8−27
@@ -2,14 +2,13 @@ package chat
22
33import (
44 "github.com/charmbracelet/bubbles/v2/key"
5− "github.com/charmbracelet/crush/internal/tui/components/core/layout"
65)
76
87type KeyMap struct {
9− NewSession key.Binding
10− FilePicker key.Binding
11− Cancel key.Binding
12− Tab key.Binding
8+ NewSession key.Binding
9+ AddAttachment key.Binding
10+ Cancel key.Binding
11+ Tab key.Binding
1312}
1413
1514func DefaultKeyMap() KeyMap {
@@ -18,6 +17,10 @@ func DefaultKeyMap() KeyMap {
1817 key.WithKeys("ctrl+n"),
1918 key.WithHelp("ctrl+n", "new session"),
2019 ),
20+ AddAttachment: key.NewBinding(
21+ key.WithKeys("ctrl+f"),
22+ key.WithHelp("ctrl+f", "add attachment"),
23+ ),
2124 Cancel: key.NewBinding(
2225 key.WithKeys("esc"),
2326 key.WithHelp("esc", "cancel"),
@@ -26,27 +29,5 @@ func DefaultKeyMap() KeyMap {
2629 key.WithKeys("tab"),
2730 key.WithHelp("tab", "change focus"),
2831 ),
29− FilePicker: key.NewBinding(
30− key.WithKeys("ctrl+f"),
31− key.WithHelp("ctrl+f", "select files to upload"),
32− ),
33− }
34−}
35−
36−// FullHelp implements help.KeyMap.
37−func (k KeyMap) FullHelp() [][]key.Binding {
38− m := [][]key.Binding{}
39− slice := layout.KeyMapToSlice(k)
40− for i := 0; i < len(slice); i += 4 {
41− end := min(i+4, len(slice))
42− m = append(m, slice[i:end])
43− }
44− return m
45−}
46−
47−// ShortHelp implements help.KeyMap.
48−func (k KeyMap) ShortHelp() []key.Binding {
49− return []key.Binding{
50− k.Tab,
5132 }
5233}
internal/tui/page/logs/logs.go+2−2
@@ -4,8 +4,8 @@ import (
44 "github.com/charmbracelet/bubbles/v2/key"
55 tea "github.com/charmbracelet/bubbletea/v2"
66 "github.com/charmbracelet/crush/internal/tui/components/core"
7− logsComponents "github.com/charmbracelet/crush/internal/tui/components/logs"
87 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
8+ logsComponents "github.com/charmbracelet/crush/internal/tui/components/logs"
99 "github.com/charmbracelet/crush/internal/tui/page"
1010 "github.com/charmbracelet/crush/internal/tui/page/chat"
1111 "github.com/charmbracelet/crush/internal/tui/styles"
@@ -37,7 +37,7 @@ func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
3737 case tea.KeyMsg:
3838 switch {
3939 case key.Matches(msg, p.keyMap.Back):
40− return p, util.CmdHandler(page.PageChangeMsg{ID: chat.ChatPage})
40+ return p, util.CmdHandler(page.PageChangeMsg{ID: chat.ChatPageID})
4141 }
4242 }
4343
internal/tui/tui.go+44−23
@@ -34,24 +34,26 @@ import (
3434
3535// appModel represents the main application model that manages pages, dialogs, and UI state.
3636type appModel struct {
37− width, height int
38− keyMap KeyMap
37+ wWidth, wHeight int // Window dimensions
38+ width, height int
39+ keyMap KeyMap
3940
4041 currentPage page.PageID
4142 previousPage page.PageID
4243 pages map[page.PageID]util.Model
4344 loadedPages map[page.PageID]bool
4445
45− status status.StatusCmp
46+ // Status
47+ status status.StatusCmp
48+ showingFullHelp bool
4649
4750 app *app.App
4851
4952 dialog dialogs.DialogCmp
5053 completions completions.Completions
5154
52− // Session
55+ // Chat Page Specific
5356 selectedSessionID string // The ID of the currently selected session
54− fullHelp bool // Whether to show full help text
5557}
5658
5759// Init initializes the application model and returns initial commands.
@@ -99,7 +101,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
99101 )
100102 return a, nil
101103 case tea.WindowSizeMsg:
102− return a, a.handleWindowResize(msg)
104+ return a, a.handleWindowResize(msg.Width, msg.Height)
103105
104106 // Completions messages
105107 case completions.OpenCompletionsMsg, completions.FilterCompletionsMsg, completions.CloseCompletionsMsg:
@@ -244,23 +246,29 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
244246}
245247
246248// handleWindowResize processes window resize events and updates all components.
247−func (a *appModel) handleWindowResize(msg tea.WindowSizeMsg) tea.Cmd {
249+func (a *appModel) handleWindowResize(width, height int) tea.Cmd {
248250 var cmds []tea.Cmd
249− msg.Height -= 2 // Make space for the status bar
250− a.width, a.height = msg.Width, msg.Height
251−
251+ a.wWidth, a.wHeight = width, height
252+ if a.showingFullHelp {
253+ height -= 3
254+ } else {
255+ height -= 2
256+ }
257+ a.width, a.height = width, height
252258 // Update status bar
253− s, cmd := a.status.Update(msg)
259+ s, cmd := a.status.Update(tea.WindowSizeMsg{Width: width, Height: height})
254260 a.status = s.(status.StatusCmp)
255261 cmds = append(cmds, cmd)
256262
257263 // Update the current page
258− updated, cmd := a.pages[a.currentPage].Update(msg)
259− a.pages[a.currentPage] = updated.(util.Model)
260− cmds = append(cmds, cmd)
264+ for p, page := range a.pages {
265+ updated, pageCmd := page.Update(tea.WindowSizeMsg{Width: width, Height: height})
266+ a.pages[p] = updated.(util.Model)
267+ cmds = append(cmds, pageCmd)
268+ }
261269
262270 // Update the dialogs
263− dialog, cmd := a.dialog.Update(msg)
271+ dialog, cmd := a.dialog.Update(tea.WindowSizeMsg{Width: width, Height: height})
264272 a.dialog = dialog.(dialogs.DialogCmp)
265273 cmds = append(cmds, cmd)
266274
@@ -288,6 +296,11 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
288296 u, cmd := a.completions.Update(msg)
289297 a.completions = u.(completions.Completions)
290298 return cmd
299+ // help
300+ case key.Matches(msg, a.keyMap.Help):
301+ a.status.ToggleFullHelp()
302+ a.showingFullHelp = !a.showingFullHelp
303+ return a.handleWindowResize(a.wWidth, a.wHeight)
291304 // dialogs
292305 case key.Matches(msg, a.keyMap.Quit):
293306 if a.dialog.ActiveDialogID() == quit.QuitDialogID {
@@ -345,7 +358,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
345358// moveToPage handles navigation between different pages in the application.
346359func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
347360 if a.app.CoderAgent.IsBusy() {
348− // For now we don't move to any page if the agent is busy
361+ // TODO: maybe remove this : For now we don't move to any page if the agent is busy
349362 return util.ReportWarn("Agent is busy, please wait...")
350363 }
351364
@@ -367,7 +380,12 @@ func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd {
367380
368381// View renders the complete application interface including pages, dialogs, and overlays.
369382func (a *appModel) View() tea.View {
370− pageView := a.pages[a.currentPage].View()
383+ page := a.pages[a.currentPage]
384+ if withHelp, ok := page.(layout.Help); ok {
385+ a.keyMap.pageBindings = withHelp.Bindings()
386+ }
387+ a.status.SetKeyMap(a.keyMap)
388+ pageView := page.View()
371389 components := []string{
372390 pageView.String(),
373391 }
@@ -412,17 +430,20 @@ func (a *appModel) View() tea.View {
412430
413431// New creates and initializes a new TUI application model.
414432func New(app *app.App) tea.Model {
415− startPage := chat.ChatPage
433+ chatPage := chat.NewChatPage(app)
434+ keyMap := DefaultKeyMap()
435+ keyMap.pageBindings = chatPage.Bindings()
436+
416437 model := &appModel{
417− currentPage: startPage,
438+ currentPage: chat.ChatPageID,
418439 app: app,
419− status: status.NewStatusCmp(),
440+ status: status.NewStatusCmp(keyMap),
420441 loadedPages: make(map[page.PageID]bool),
421− keyMap: DefaultKeyMap(),
442+ keyMap: keyMap,
422443
423444 pages: map[page.PageID]util.Model{
424− chat.ChatPage: chat.NewChatPage(app),
425− logs.LogsPage: logs.NewLogsPage(),
445+ chat.ChatPageID: chatPage,
446+ logs.LogsPage: logs.NewLogsPage(),
426447 },
427448
428449 dialog: dialogs.NewDialogCmp(),
todos.md+16−7
@@ -1,11 +1,18 @@
11## TODOs before release
22
3−- [~] Implement help
4− - [ ] Show full help
5− - [ ] Make help dependent on the focused pane and page
3+- [x] Implement help
4+ - [x] Show full help
5+ - [x] Make help dependent on the focused pane and page
6+- [ ] Implement current model in the sidebar
7+- [ ] Implement changed files
68- [ ] Events when tool error
7−- [ ] Fix issue with numbers (padding)
8−- [ ] Fancy Spinner
9+- [ ] Support bash commands
10+- [ ] Editor attachments fixes
11+ - [ ] Reimplement removing attachments
12+- [ ] Fix the logs view
13+ - [ ] Review the implementation
14+ - [ ] The page lags
15+ - [ ] Make the logs long lived ?
916- [ ] Add all possible actions to the commands
1017- [ ] Parallel tool calls and permissions
1118 - [ ] Run the tools in parallel and add results in parallel
@@ -14,14 +21,16 @@
1421 - [ ] Weird behavior sometimes the message does not update
1522 - [ ] Message length (I saw the message go beyond the correct length when there are errors)
1623 - [ ] Address UX issues
17−- [ ] Implement current model in the sidebar
18−- [ ] Implement changed files
24+ - [ ] Fix issue with numbers (padding) view tool
1925- [ ] Implement responsive mode
2026- [ ] Revisit the core list component
2127 - [ ] This component has become super complex we might need to fix this.
28+- [ ] Investigate ways to make the spinner less CPU intensive
2229- [ ] General cleanup and documentation
2330- [ ] Update the readme
2431
2532## Maybe
2633
2734- [ ] Revisit the provider/model/configs
35+- [ ] Implement correct persistent shell
36+- [ ] 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