on main help sign in
fix: conditionally show image keybindings based on model support (#2522)
commitf416931307d5b446dfe1e376d9c0ade4a28b6406 authorhuaiyuWangh <34158348+huaiyuWangh@users.noreply.github.com> dateWed 1 Apr 2026, 03:24 (7 months ago) parent5cf6e5a
2 files changed+144−16
internal/ui/model/ui.go+37−16 internal/ui/model/ui_test.go+107
internal/ui/model/ui.go+37−16
@@ -1722,11 +1722,17 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
17221722
17231723 switch {
17241724 case key.Matches(msg, m.keyMap.Editor.AddImage):
1725+ if !m.currentModelSupportsImages() {
1726+ break
1727+ }
17251728 if cmd := m.openFilesDialog(); cmd != nil {
17261729 cmds = append(cmds, cmd)
17271730 }
17281731
17291732 case key.Matches(msg, m.keyMap.Editor.PasteImage):
1733+ if !m.currentModelSupportsImages() {
1734+ break
1735+ }
17301736 cmds = append(cmds, m.pasteImageFromClipboard)
17311737
17321738 case key.Matches(msg, m.keyMap.Editor.SendMessage):
@@ -2259,15 +2265,15 @@ func (m *UI) FullHelp() [][]key.Binding {
22592265
22602266 switch m.focus {
22612267 case uiFocusEditor:
2262− binds = append(binds,
2263− []key.Binding{
2264− k.Editor.Newline,
2265− k.Editor.AddImage,
2266− k.Editor.PasteImage,
2267− k.Editor.MentionFile,
2268− k.Editor.OpenEditor,
2269− },
2270− )
2268+ editorBinds := []key.Binding{
2269+ k.Editor.Newline,
2270+ k.Editor.MentionFile,
2271+ k.Editor.OpenEditor,
2272+ }
2273+ if m.currentModelSupportsImages() {
2274+ editorBinds = append(editorBinds, k.Editor.AddImage, k.Editor.PasteImage)
2275+ }
2276+ binds = append(binds, editorBinds)
22712277 if hasAttachments {
22722278 binds = append(binds,
22732279 []key.Binding{
@@ -2309,14 +2315,16 @@ func (m *UI) FullHelp() [][]key.Binding {
23092315 k.Models,
23102316 k.Sessions,
23112317 },
2312− []key.Binding{
2313− k.Editor.Newline,
2314− k.Editor.AddImage,
2315− k.Editor.PasteImage,
2316− k.Editor.MentionFile,
2317− k.Editor.OpenEditor,
2318− },
23192318 )
2319+ editorBinds := []key.Binding{
2320+ k.Editor.Newline,
2321+ k.Editor.MentionFile,
2322+ k.Editor.OpenEditor,
2323+ }
2324+ if m.currentModelSupportsImages() {
2325+ editorBinds = append(editorBinds, k.Editor.AddImage, k.Editor.PasteImage)
2326+ }
2327+ binds = append(binds, editorBinds)
23202328 if hasAttachments {
23212329 binds = append(binds,
23222330 []key.Binding{
@@ -2339,6 +2347,19 @@ func (m *UI) FullHelp() [][]key.Binding {
23392347 return binds
23402348}
23412349
2350+func (m *UI) currentModelSupportsImages() bool {
2351+ cfg := m.com.Config()
2352+ if cfg == nil {
2353+ return false
2354+ }
2355+ agentCfg, ok := cfg.Agents[config.AgentCoder]
2356+ if !ok {
2357+ return false
2358+ }
2359+ model := cfg.GetModelByType(agentCfg.Model)
2360+ return model != nil && model.SupportsImages
2361+}
2362+
23422363// toggleCompactMode toggles compact mode between uiChat and uiChatCompact states.
23432364func (m *UI) toggleCompactMode() tea.Cmd {
23442365 m.forceCompactMode = !m.forceCompactMode
internal/ui/model/ui_test.gonew file+107
@@ -0,0 +1,107 @@
1+package model
2+
3+import (
4+ "reflect"
5+ "testing"
6+ "unsafe"
7+
8+ "charm.land/catwalk/pkg/catwalk"
9+ "github.com/charmbracelet/crush/internal/app"
10+ "github.com/charmbracelet/crush/internal/config"
11+ "github.com/charmbracelet/crush/internal/csync"
12+ "github.com/charmbracelet/crush/internal/ui/common"
13+ "github.com/stretchr/testify/require"
14+)
15+
16+func TestCurrentModelSupportsImages(t *testing.T) {
17+ t.Parallel()
18+
19+ t.Run("returns false when config is nil", func(t *testing.T) {
20+ t.Parallel()
21+
22+ ui := newTestUIWithConfig(t, nil)
23+ require.False(t, ui.currentModelSupportsImages())
24+ })
25+
26+ t.Run("returns false when coder agent is missing", func(t *testing.T) {
27+ t.Parallel()
28+
29+ cfg := &config.Config{
30+ Providers: csync.NewMap[string, config.ProviderConfig](),
31+ Agents: map[string]config.Agent{},
32+ }
33+ ui := newTestUIWithConfig(t, cfg)
34+ require.False(t, ui.currentModelSupportsImages())
35+ })
36+
37+ t.Run("returns false when model is not found", func(t *testing.T) {
38+ t.Parallel()
39+
40+ cfg := &config.Config{
41+ Providers: csync.NewMap[string, config.ProviderConfig](),
42+ Agents: map[string]config.Agent{
43+ config.AgentCoder: {Model: config.SelectedModelTypeLarge},
44+ },
45+ }
46+ ui := newTestUIWithConfig(t, cfg)
47+ require.False(t, ui.currentModelSupportsImages())
48+ })
49+
50+ t.Run("returns true when current model supports images", func(t *testing.T) {
51+ t.Parallel()
52+
53+ providers := csync.NewMap[string, config.ProviderConfig]()
54+ providers.Set("test-provider", config.ProviderConfig{
55+ ID: "test-provider",
56+ Models: []catwalk.Model{
57+ {ID: "test-model", SupportsImages: true},
58+ },
59+ })
60+
61+ cfg := &config.Config{
62+ Models: map[config.SelectedModelType]config.SelectedModel{
63+ config.SelectedModelTypeLarge: {
64+ Provider: "test-provider",
65+ Model: "test-model",
66+ },
67+ },
68+ Providers: providers,
69+ Agents: map[string]config.Agent{
70+ config.AgentCoder: {Model: config.SelectedModelTypeLarge},
71+ },
72+ }
73+
74+ ui := newTestUIWithConfig(t, cfg)
75+ require.True(t, ui.currentModelSupportsImages())
76+ })
77+}
78+
79+func newTestUIWithConfig(t *testing.T, cfg *config.Config) *UI {
80+ t.Helper()
81+
82+ store := &config.ConfigStore{}
83+ setUnexportedField(t, store, "config", cfg)
84+
85+ appInstance := &app.App{}
86+ setUnexportedField(t, appInstance, "config", store)
87+
88+ return &UI{
89+ com: &common.Common{
90+ App: appInstance,
91+ },
92+ }
93+}
94+
95+func setUnexportedField(t *testing.T, target any, name string, value any) {
96+ t.Helper()
97+
98+ v := reflect.ValueOf(target)
99+ require.Equal(t, reflect.Pointer, v.Kind())
100+ require.False(t, v.IsNil())
101+
102+ field := v.Elem().FieldByName(name)
103+ require.Truef(t, field.IsValid(), "field %q not found", name)
104+
105+ fieldValue := reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem()
106+ fieldValue.Set(reflect.ValueOf(value))
107+}