Detailed changes
@@ -166,7 +166,7 @@ func (o *openaiClient) preparedParams(messages []openai.ChatCompletionMessagePar
Tools: tools,
}
- if o.providerOptions.model.CanReason == true {
+ if o.providerOptions.model.CanReason {
params.MaxCompletionTokens = openai.Int(o.providerOptions.maxTokens)
switch o.options.reasoningEffort {
case "low":
@@ -0,0 +1,83 @@
+package shell
+
+import (
+ "context"
+ "os"
+ "runtime"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestShellPerformanceComparison(t *testing.T) {
+ tmpDir, err := os.MkdirTemp("", "shell-test")
+ require.NoError(t, err)
+ defer os.RemoveAll(tmpDir)
+
+ shell := GetPersistentShell(tmpDir)
+ defer shell.Close()
+
+ // Test quick command
+ start := time.Now()
+ stdout, stderr, exitCode, _, err := shell.Exec(context.Background(), "echo 'hello'", 0)
+ duration := time.Since(start)
+
+ require.NoError(t, err)
+ assert.Equal(t, 0, exitCode)
+ assert.Contains(t, stdout, "hello")
+ assert.Empty(t, stderr)
+
+ t.Logf("Quick command took: %v", duration)
+}
+
+func TestShellCPUUsageComparison(t *testing.T) {
+ tmpDir, err := os.MkdirTemp("", "shell-test")
+ require.NoError(t, err)
+ defer os.RemoveAll(tmpDir)
+
+ shell := GetPersistentShell(tmpDir)
+ defer shell.Close()
+
+ // Measure CPU and memory usage during a longer command
+ var m1, m2 runtime.MemStats
+ runtime.GC()
+ runtime.ReadMemStats(&m1)
+
+ start := time.Now()
+ _, stderr, exitCode, _, err := shell.Exec(context.Background(), "sleep 0.1", 1000)
+ duration := time.Since(start)
+
+ runtime.ReadMemStats(&m2)
+
+ require.NoError(t, err)
+ assert.Equal(t, 0, exitCode)
+ assert.Empty(t, stderr)
+
+ memGrowth := m2.Alloc - m1.Alloc
+ t.Logf("Sleep 0.1s command took: %v", duration)
+ t.Logf("Memory growth during polling: %d bytes", memGrowth)
+ t.Logf("GC cycles during test: %d", m2.NumGC-m1.NumGC)
+}
+
+// Benchmark CPU usage during polling
+func BenchmarkShellPolling(b *testing.B) {
+ tmpDir, err := os.MkdirTemp("", "shell-bench")
+ require.NoError(b, err)
+ defer os.RemoveAll(tmpDir)
+
+ shell := GetPersistentShell(tmpDir)
+ defer shell.Close()
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for i := 0; i < b.N; i++ {
+ // Use a short sleep to measure polling overhead
+ _, _, exitCode, _, err := shell.Exec(context.Background(), "sleep 0.02", 500)
+ if err != nil || exitCode != 0 {
+ b.Fatalf("Command failed: %v, exit code: %d", err, exitCode)
+ }
+ }
+}
@@ -189,6 +189,13 @@ echo $EXEC_EXIT_CODE > %s
done := make(chan bool)
go func() {
+ // Use exponential backoff polling
+ pollInterval := 1 * time.Millisecond
+ maxPollInterval := 100 * time.Millisecond
+
+ ticker := time.NewTicker(pollInterval)
+ defer ticker.Stop()
+
for {
select {
case <-ctx.Done():
@@ -197,7 +204,7 @@ echo $EXEC_EXIT_CODE > %s
done <- true
return
- case <-time.After(10 * time.Millisecond):
+ case <-ticker.C:
if fileExists(statusFile) && fileSize(statusFile) > 0 {
done <- true
return
@@ -212,6 +219,15 @@ echo $EXEC_EXIT_CODE > %s
return
}
}
+
+ // Exponential backoff to reduce CPU usage for longer-running commands
+ if pollInterval < maxPollInterval {
+ pollInterval = time.Duration(float64(pollInterval) * 1.5)
+ if pollInterval > maxPollInterval {
+ pollInterval = maxPollInterval
+ }
+ ticker.Reset(pollInterval)
+ }
}
}
}()
@@ -0,0 +1,54 @@
+package shell
+
+import (
+ "context"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestShellPerformanceImprovement(t *testing.T) {
+ // Create a temporary directory for the shell
+ tmpDir, err := os.MkdirTemp("", "shell-test")
+ require.NoError(t, err)
+ defer os.RemoveAll(tmpDir)
+
+ shell := GetPersistentShell(tmpDir)
+ defer shell.Close()
+
+ // Test that quick commands complete fast
+ start := time.Now()
+ stdout, stderr, exitCode, _, err := shell.Exec(context.Background(), "echo 'hello world'", 0)
+ duration := time.Since(start)
+
+ require.NoError(t, err)
+ assert.Equal(t, 0, exitCode)
+ assert.Contains(t, stdout, "hello world")
+ assert.Empty(t, stderr)
+
+ // Quick commands should complete very fast with our exponential backoff
+ assert.Less(t, duration, 50*time.Millisecond, "Quick command should complete fast with exponential backoff")
+}
+
+// Benchmark to measure CPU efficiency
+func BenchmarkShellQuickCommands(b *testing.B) {
+ tmpDir, err := os.MkdirTemp("", "shell-bench")
+ require.NoError(b, err)
+ defer os.RemoveAll(tmpDir)
+
+ shell := GetPersistentShell(tmpDir)
+ defer shell.Close()
+
+ b.ResetTimer()
+ b.ReportAllocs()
+
+ for i := 0; i < b.N; i++ {
+ _, _, exitCode, _, err := shell.Exec(context.Background(), "echo test", 0)
+ if err != nil || exitCode != 0 {
+ b.Fatalf("Command failed: %v, exit code: %d", err, exitCode)
+ }
+ }
+}
@@ -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"
@@ -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
+}
@@ -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()
@@ -5,13 +5,18 @@ import (
)
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"),
),
@@ -22,31 +27,10 @@ func DefaultEditorKeyMap() EditorKeyMap {
}
}
-// KeyBindings implements layout.KeyMapProvider
-func (k EditorKeyMap) KeyBindings() []key.Binding {
- return []key.Binding{
- k.Send,
- k.OpenEditor,
- }
-}
-
-// FullHelp implements help.KeyMap.
-func (k EditorKeyMap) FullHelp() [][]key.Binding {
- m := [][]key.Binding{}
- slice := k.KeyBindings()
- 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,
- }
+type DeleteAttachmentKeyMaps struct {
+ AttachmentDeleteMode key.Binding
+ Escape key.Binding
+ DeleteAllAttachments key.Binding
}
// TODO: update this to use the new keymap concepts
@@ -63,4 +47,4 @@ var AttachmentsKeyMaps = DeleteAttachmentKeyMaps{
key.WithKeys("r"),
key.WithHelp("ctrl+r+r", "delete all attachments"),
),
-}
+}
@@ -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
}
@@ -1,7 +1,8 @@
package layout
import (
- "github.com/charmbracelet/bubbles/v2/help"
+ "reflect"
+
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
)
@@ -18,7 +19,7 @@ type Sizeable interface {
}
type Help interface {
- Help() help.KeyMap
+ Bindings() []key.Binding
}
type Positionable interface {
@@ -28,4 +29,4 @@ type Positionable interface {
// KeyMapProvider defines an interface for types that can provide their key bindings as a slice
type KeyMapProvider interface {
KeyBindings() []key.Binding
-}
+}
@@ -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
@@ -50,39 +50,4 @@ func DefaultKeyMap() KeyMap {
key.WithKeys("shift+g", "end"),
),
}
-}
-
-// KeyBindings implements layout.KeyMapProvider
-func (k KeyMap) KeyBindings() []key.Binding {
- return []key.Binding{
- k.Down,
- k.Up,
- k.NDown,
- k.NUp,
- k.DownOneItem,
- k.UpOneItem,
- k.HalfPageDown,
- k.HalfPageUp,
- k.Home,
- k.End,
- }
-}
-
-// FullHelp implements help.KeyMap.
-func (k KeyMap) FullHelp() [][]key.Binding {
- m := [][]key.Binding{}
- slice := k.KeyBindings()
- 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,
- }
-}
+}
@@ -1,64 +0,0 @@
-package status
-
-import (
- "github.com/charmbracelet/bubbles/v2/key"
-)
-
-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"),
- ),
- }
-}
-
-// KeyBindings implements layout.KeyMapProvider
-func (k KeyMap) KeyBindings() []key.Binding {
- return []key.Binding{
- k.Tab,
- k.Commands,
- k.Sessions,
- k.Help,
- }
-}
-
-// FullHelp implements help.KeyMap.
-func (k KeyMap) FullHelp() [][]key.Binding {
- m := [][]key.Binding{}
- slice := k.KeyBindings()
- 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,
- }
-}
@@ -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,
}
}
@@ -10,6 +10,8 @@ type KeyMap struct {
Help key.Binding
Commands key.Binding
Sessions key.Binding
+
+ pageBindings []key.Binding
}
func DefaultKeyMap() KeyMap {
@@ -22,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"),
@@ -41,26 +42,59 @@ func DefaultKeyMap() KeyMap {
// FullHelp implements help.KeyMap.
func (k KeyMap) FullHelp() [][]key.Binding {
m := [][]key.Binding{}
- slice := k.KeyBindings()
- 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
}
-// KeyBindings implements layout.KeyMapProvider
-func (k KeyMap) KeyBindings() []key.Binding {
- return []key.Binding{
- k.Logs,
- k.Quit,
- k.Help,
- k.Commands,
- k.Sessions,
+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)
+}
@@ -19,26 +19,31 @@ import (
"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),
@@ -5,10 +5,10 @@ import (
)
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 {
@@ -17,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"),
@@ -25,37 +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"),
- ),
- }
-}
-
-// KeyBindings implements layout.KeyMapProvider
-func (k KeyMap) KeyBindings() []key.Binding {
- return []key.Binding{
- k.NewSession,
- k.FilePicker,
- k.Cancel,
- k.Tab,
}
-}
-
-// FullHelp implements help.KeyMap.
-func (k KeyMap) FullHelp() [][]key.Binding {
- m := [][]key.Binding{}
- slice := k.KeyBindings()
- 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,
- }
-}
+}
@@ -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})
}
}
@@ -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(),
@@ -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