From 0b745ae4fc3e9cf37f70430eb1ed29a50dc339fa Mon Sep 17 00:00:00 2001 From: Djordje Lukic Date: Wed, 20 May 2026 00:18:03 +0200 Subject: [PATCH] Treat wezterm as a terminal that knows how to handle shift+enter Signed-off-by: Djordje Lukic --- pkg/tui/components/editor/editor.go | 5 +- pkg/tui/internal/termfeatures/keyboard.go | 19 +++++++ .../internal/termfeatures/keyboard_test.go | 35 +++++++++++++ pkg/tui/tui.go | 49 ++++++++++--------- pkg/tui/tui_helpers_test.go | 23 +++++++++ 5 files changed, 106 insertions(+), 25 deletions(-) create mode 100644 pkg/tui/internal/termfeatures/keyboard.go create mode 100644 pkg/tui/internal/termfeatures/keyboard_test.go diff --git a/pkg/tui/components/editor/editor.go b/pkg/tui/components/editor/editor.go index 4071503e7..a4e1fbcf5 100644 --- a/pkg/tui/components/editor/editor.go +++ b/pkg/tui/components/editor/editor.go @@ -27,6 +27,7 @@ import ( "github.com/docker/docker-agent/pkg/tui/components/editor/completions" "github.com/docker/docker-agent/pkg/tui/core" "github.com/docker/docker-agent/pkg/tui/core/layout" + "github.com/docker/docker-agent/pkg/tui/internal/termfeatures" "github.com/docker/docker-agent/pkg/tui/messages" "github.com/docker/docker-agent/pkg/tui/styles" ) @@ -195,7 +196,7 @@ func New(hist *history.History, opts ...Option) Editor { textarea: ta, searchInput: si, hist: hist, - keyboardEnhancementsSupported: false, + keyboardEnhancementsSupported: termfeatures.SupportsModifiedEnter(os.Getenv), banner: newAttachmentBanner(), } @@ -634,7 +635,7 @@ func (e *editor) Update(msg tea.Msg) (layout.Model, tea.Cmd) { } case tea.KeyboardEnhancementsMsg: // Track keyboard enhancement support and configure newline keybinding accordingly - e.keyboardEnhancementsSupported = msg.Flags != 0 + e.keyboardEnhancementsSupported = msg.Flags != 0 || termfeatures.SupportsModifiedEnter(os.Getenv) e.configureNewlineKeybinding() return e, nil case messages.ThemeChangedMsg: diff --git a/pkg/tui/internal/termfeatures/keyboard.go b/pkg/tui/internal/termfeatures/keyboard.go new file mode 100644 index 000000000..dc0d64efb --- /dev/null +++ b/pkg/tui/internal/termfeatures/keyboard.go @@ -0,0 +1,19 @@ +package termfeatures + +import "strings" + +// SupportsModifiedEnter returns true for terminals that can distinguish +// Shift+Enter from Enter even when they do not report Kitty keyboard flags. +func SupportsModifiedEnter(getenv func(string) string) bool { + if getenv == nil { + return false + } + + termProgram := strings.ToLower(getenv("TERM_PROGRAM")) + term := strings.ToLower(getenv("TERM")) + + return termProgram == "wezterm" || + getenv("WEZTERM_PANE") != "" || + getenv("WEZTERM_UNIX_SOCKET") != "" || + strings.Contains(term, "wezterm") +} diff --git a/pkg/tui/internal/termfeatures/keyboard_test.go b/pkg/tui/internal/termfeatures/keyboard_test.go new file mode 100644 index 000000000..e2bed09e5 --- /dev/null +++ b/pkg/tui/internal/termfeatures/keyboard_test.go @@ -0,0 +1,35 @@ +package termfeatures + +import "testing" + +func TestSupportsModifiedEnter(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + env map[string]string + want bool + }{ + {name: "wezterm term program", env: map[string]string{"TERM_PROGRAM": "WezTerm"}, want: true}, + {name: "wezterm pane", env: map[string]string{"WEZTERM_PANE": "1"}, want: true}, + {name: "wezterm socket", env: map[string]string{"WEZTERM_UNIX_SOCKET": "/tmp/wezterm.sock"}, want: true}, + {name: "wezterm term", env: map[string]string{"TERM": "wezterm"}, want: true}, + {name: "other terminal", env: map[string]string{"TERM_PROGRAM": "Apple_Terminal", "TERM": "xterm-256color"}, want: false}, + {name: "nil getenv", env: nil, want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var getenv func(string) string + if tt.env != nil { + getenv = func(key string) string { return tt.env[key] } + } + + if got := SupportsModifiedEnter(getenv); got != tt.want { + t.Fatalf("SupportsModifiedEnter() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index f24e3680f..8bb8feb24 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -34,6 +34,7 @@ import ( "github.com/docker/docker-agent/pkg/tui/core" "github.com/docker/docker-agent/pkg/tui/dialog" "github.com/docker/docker-agent/pkg/tui/internal/editorname" + "github.com/docker/docker-agent/pkg/tui/internal/termfeatures" "github.com/docker/docker-agent/pkg/tui/messages" "github.com/docker/docker-agent/pkg/tui/page/chat" "github.com/docker/docker-agent/pkg/tui/service" @@ -287,28 +288,29 @@ func New(ctx context.Context, spawner SessionSpawner, initialApp *app.App, initi buildCommandCategories: func(ctx context.Context, _ tea.Model) []commands.Category { return commands.BuildCommandCategories(ctx, initialApp) }, - supervisor: sv, - tabBar: tb, - tuiStore: ts, - chatPages: map[string]chat.Page{}, - editors: map[string]editor.Editor{}, - sessionStates: map[string]*service.SessionState{sessID: initialSessionState}, - application: initialApp, - sessionState: initialSessionState, - history: historyStore, - pendingRestores: make(map[string]string), - pendingSidebarCollapsed: make(map[string]bool), - stashedDialogs: make(map[string]stashedDialog), - notification: notification.New(), - dialogMgr: dialog.New(), - completions: completion.New(), - transcriber: transcribe.New(os.Getenv("OPENAI_API_KEY")), - workingSpinner: spinner.New(spinner.ModeSpinnerOnly, styles.SpinnerDotsHighlightStyle), - focusedPanel: PanelEditor, - editorLines: 3, - dockerDesktop: os.Getenv("TERM_PROGRAM") == "docker_desktop", - appName: "docker agent", - appVersion: version.Version, + supervisor: sv, + tabBar: tb, + tuiStore: ts, + chatPages: map[string]chat.Page{}, + editors: map[string]editor.Editor{}, + sessionStates: map[string]*service.SessionState{sessID: initialSessionState}, + application: initialApp, + sessionState: initialSessionState, + history: historyStore, + pendingRestores: make(map[string]string), + pendingSidebarCollapsed: make(map[string]bool), + stashedDialogs: make(map[string]stashedDialog), + notification: notification.New(), + dialogMgr: dialog.New(), + completions: completion.New(), + transcriber: transcribe.New(os.Getenv("OPENAI_API_KEY")), + workingSpinner: spinner.New(spinner.ModeSpinnerOnly, styles.SpinnerDotsHighlightStyle), + focusedPanel: PanelEditor, + editorLines: 3, + keyboardEnhancementsSupported: termfeatures.SupportsModifiedEnter(os.Getenv), + dockerDesktop: os.Getenv("TERM_PROGRAM") == "docker_desktop", + appName: "docker agent", + appVersion: version.Version, } // Apply options @@ -651,7 +653,8 @@ func (m *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyboardEnhancementsMsg: m.keyboardEnhancements = &msg - m.keyboardEnhancementsSupported = msg.Flags != 0 + m.keyboardEnhancementsSupported = msg.Flags != 0 || termfeatures.SupportsModifiedEnter(os.Getenv) + m.statusBar.InvalidateCache() return m, tea.Batch(m.updateChatCmd(msg), m.updateEditorCmd(msg)) // --- Keyboard input --- diff --git a/pkg/tui/tui_helpers_test.go b/pkg/tui/tui_helpers_test.go index 051d604f6..56d0d59c2 100644 --- a/pkg/tui/tui_helpers_test.go +++ b/pkg/tui/tui_helpers_test.go @@ -5,8 +5,31 @@ import ( "testing" tea "charm.land/bubbletea/v2" + + "github.com/docker/docker-agent/pkg/tui/components/statusbar" + "github.com/docker/docker-agent/pkg/tui/components/tabbar" ) +func TestKeyboardEnhancementsInvalidateStatusBarHelp(t *testing.T) { + m, _ := newTestModel() + m.focusedPanel = PanelEditor + m.tabBar = tabbar.New(0) + m.statusBar = statusbar.New(m) + m.statusBar.SetWidth(400) + + before := m.statusBar.View() + if !strings.Contains(before, "Ctrl+j") { + t.Fatalf("status bar before keyboard enhancements = %q, want Ctrl+j newline help", before) + } + + _, _ = m.Update(tea.KeyboardEnhancementsMsg{Flags: 1}) + + after := m.statusBar.View() + if !strings.Contains(after, "Shift+Enter") { + t.Fatalf("status bar after keyboard enhancements = %q, want Shift+Enter newline help", after) + } +} + func TestParseCtrlNumberKey(t *testing.T) { t.Parallel()