on main help sign in
fix: conditionally show image keybindings based on model support (#2522)
f416931huaiyuWangh
internal/ui/model/ui.go+37−16
@@ -1722,11 +1722,17 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
1722
1722
1723 switch {
1723 switch {
1724 case key.Matches(msg, m.keyMap.Editor.AddImage):
1724 case key.Matches(msg, m.keyMap.Editor.AddImage):
1725+ if !m.currentModelSupportsImages() {
1726+ break
1727+ }
1725 if cmd := m.openFilesDialog(); cmd != nil {
1728 if cmd := m.openFilesDialog(); cmd != nil {
1726 cmds = append(cmds, cmd)
1729 cmds = append(cmds, cmd)
1727 }
1730 }
1728
1731
1729 case key.Matches(msg, m.keyMap.Editor.PasteImage):
1732 case key.Matches(msg, m.keyMap.Editor.PasteImage):
1733+ if !m.currentModelSupportsImages() {
1734+ break
1735+ }
1730 cmds = append(cmds, m.pasteImageFromClipboard)
1736 cmds = append(cmds, m.pasteImageFromClipboard)
1731
1737
1732 case key.Matches(msg, m.keyMap.Editor.SendMessage):
1738 case key.Matches(msg, m.keyMap.Editor.SendMessage):
@@ -2259,15 +2265,15 @@ func (m *UI) FullHelp() [][]key.Binding {
2259
2265
2260 switch m.focus {
2266 switch m.focus {
2261 case uiFocusEditor:
2267 case uiFocusEditor:
2262− binds = append(binds,
2263− []key.Binding{
2268+ editorBinds := []key.Binding{
2264− k.Editor.Newline,
2269+ k.Editor.Newline,
2265− k.Editor.AddImage,
2270+ k.Editor.MentionFile,
2271+ k.Editor.OpenEditor,
2272+ }
2273+ if m.currentModelSupportsImages() {
2266− k.Editor.PasteImage,
2274+ editorBinds = append(editorBinds, k.Editor.AddImage, k.Editor.PasteImage)
2267− k.Editor.MentionFile,
2268− k.Editor.OpenEditor,
2269− },
2275+ }
2270− )
2276+ binds = append(binds, editorBinds)
2271 if hasAttachments {
2277 if hasAttachments {
2272 binds = append(binds,
2278 binds = append(binds,
2273 []key.Binding{
2279 []key.Binding{
@@ -2309,14 +2315,16 @@ func (m *UI) FullHelp() [][]key.Binding {
2309 k.Models,
2315 k.Models,
2310 k.Sessions,
2316 k.Sessions,
2311 },
2317 },
2312− []key.Binding{
2313− k.Editor.Newline,
2314− k.Editor.AddImage,
2315− k.Editor.PasteImage,
2316− k.Editor.MentionFile,
2317− k.Editor.OpenEditor,
2318− },
2319 )
2318 )
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)
2320 if hasAttachments {
2328 if hasAttachments {
2321 binds = append(binds,
2329 binds = append(binds,
2322 []key.Binding{
2330 []key.Binding{
@@ -2339,6 +2347,19 @@ func (m *UI) FullHelp() [][]key.Binding {
2339 return binds
2347 return binds
2340}
2348}
2341
2349
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+
2342// toggleCompactMode toggles compact mode between uiChat and uiChatCompact states.
2363// toggleCompactMode toggles compact mode between uiChat and uiChatCompact states.
2343func (m *UI) toggleCompactMode() tea.Cmd {
2364func (m *UI) toggleCompactMode() tea.Cmd {
2344 m.forceCompactMode = !m.forceCompactMode
2365 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+}