on main help sign in
feat: add attachments to messages
commitf44035ea80fe213874063f9af6aa5a6a807b3e2b authorKujtim Hoxha <kujtimii.h@gmail.com> dateMon 9 Jun 2025, 12:19 (1 year ago) parentb744b0f
5 files changed+83−42
internal/tui/components/chat/editor/editor.go+26−36 internal/tui/components/core/status/status.go+1−1 internal/tui/components/dialogs/filepicker/filepicker.go+44−2 internal/tui/page/chat/chat.go+11−1 internal/tui/tui.go+1−2
internal/tui/components/chat/editor/editor.go+26−36
@@ -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/dialogs/filepicker"
2122 "github.com/charmbracelet/crush/internal/tui/layout"
2223 "github.com/charmbracelet/crush/internal/tui/styles"
2324 "github.com/charmbracelet/crush/internal/tui/util"
@@ -141,13 +142,13 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
141142 m.session = msg
142143 }
143144 return m, nil
144− // case dialog.AttachmentAddedMsg:
145− // if len(m.attachments) >= maxAttachments {
146− // logging.ErrorPersist(fmt.Sprintf("cannot add more than %d images", maxAttachments))
147− // return m, cmd
148− // }
149− // m.attachments = append(m.attachments, msg.Attachment)
150− // return m, nil
145+ case filepicker.FilePickedMsg:
146+ if len(m.attachments) >= maxAttachments {
147+ logging.ErrorPersist(fmt.Sprintf("cannot add more than %d images", maxAttachments))
148+ return m, cmd
149+ }
150+ m.attachments = append(m.attachments, msg.Attachment)
151+ return m, nil
151152 case completions.CompletionsClosedMsg:
152153 m.isCompletionsOpen = false
153154 m.currentQuery = ""
@@ -351,7 +352,24 @@ func (m *editorCmp) startCompletions() tea.Msg {
351352 }
352353}
353354
354−func CreateTextArea(existing *textarea.Model) textarea.Model {
355+// Blur implements Container.
356+func (c *editorCmp) Blur() tea.Cmd {
357+ c.textarea.Blur()
358+ return nil
359+}
360+
361+// Focus implements Container.
362+func (c *editorCmp) Focus() tea.Cmd {
363+ logging.Info("Focusing editor textarea")
364+ return c.textarea.Focus()
365+}
366+
367+// IsFocused implements Container.
368+func (c *editorCmp) IsFocused() bool {
369+ return c.textarea.Focused()
370+}
371+
372+func NewEditorCmp(app *app.App) util.Model {
355373 t := styles.CurrentTheme()
356374 ta := textarea.New()
357375 ta.SetStyles(t.S().TextArea)
@@ -369,36 +387,8 @@ func CreateTextArea(existing *textarea.Model) textarea.Model {
369387 ta.CharLimit = -1
370388 ta.Placeholder = "Tell me more about this project..."
371389 ta.SetVirtualCursor(false)
372−
373− if existing != nil {
374− ta.SetValue(existing.Value())
375− ta.SetWidth(existing.Width())
376− ta.SetHeight(existing.Height())
377− }
378−
379390 ta.Focus()
380− return ta
381−}
382391
383−// Blur implements Container.
384−func (c *editorCmp) Blur() tea.Cmd {
385− c.textarea.Blur()
386− return nil
387−}
388−
389−// Focus implements Container.
390−func (c *editorCmp) Focus() tea.Cmd {
391− logging.Info("Focusing editor textarea")
392− return c.textarea.Focus()
393−}
394−
395−// IsFocused implements Container.
396−func (c *editorCmp) IsFocused() bool {
397− return c.textarea.Focused()
398−}
399−
400−func NewEditorCmp(app *app.App) util.Model {
401− ta := CreateTextArea(nil)
402392 return &editorCmp{
403393 app: app,
404394 textarea: ta,
internal/tui/components/core/status/status.go+1−1
@@ -88,7 +88,7 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8888
8989func (m statusCmp) View() tea.View {
9090 t := styles.CurrentTheme()
91− status := t.S().Base.Padding(0, 1).Render(m.help.View(DefaultKeyMap("focus chat")))
91+ status := t.S().Base.Padding(0, 1, 1, 1).Render(m.help.View(DefaultKeyMap("focus chat")))
9292 if m.info.Msg != "" {
9393 switch m.info.Type {
9494 case util.InfoTypeError:
internal/tui/components/dialogs/filepicker/filepicker.go+44−2
@@ -1,13 +1,18 @@
11package filepicker
22
33import (
4+ "fmt"
5+ "net/http"
46 "os"
7+ "path/filepath"
58 "strings"
69
710 "github.com/charmbracelet/bubbles/v2/filepicker"
811 "github.com/charmbracelet/bubbles/v2/help"
912 "github.com/charmbracelet/bubbles/v2/key"
1013 tea "github.com/charmbracelet/bubbletea/v2"
14+ "github.com/charmbracelet/crush/internal/logging"
15+ "github.com/charmbracelet/crush/internal/message"
1116 "github.com/charmbracelet/crush/internal/tui/components/core"
1217 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
1318 "github.com/charmbracelet/crush/internal/tui/components/image"
@@ -23,7 +28,7 @@ const (
2328)
2429
2530type FilePickedMsg struct {
26− FilePath string
31+ Attachment message.Attachment
2732}
2833
2934type FilePicker interface {
@@ -111,7 +116,31 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
111116 // Get the path of the selected file.
112117 return m, tea.Sequence(
113118 util.CmdHandler(dialogs.CloseDialogMsg{}),
114− util.CmdHandler(FilePickedMsg{FilePath: path}),
119+ func() tea.Msg {
120+ isFileLarge, err := ValidateFileSize(path, maxAttachmentSize)
121+ if err != nil {
122+ logging.ErrorPersist("unable to read the image")
123+ return nil
124+ }
125+ if isFileLarge {
126+ logging.ErrorPersist("file too large, max 5MB")
127+ return nil
128+ }
129+
130+ content, err := os.ReadFile(path)
131+ if err != nil {
132+ logging.ErrorPersist("Unable read selected file")
133+ return nil
134+ }
135+
136+ mimeBufferSize := min(512, len(content))
137+ mimeType := http.DetectContentType(content[:mimeBufferSize])
138+ fileName := filepath.Base(path)
139+ attachment := message.Attachment{FilePath: path, FileName: fileName, MimeType: mimeType, Content: content}
140+ return FilePickedMsg{
141+ Attachment: attachment,
142+ }
143+ },
115144 )
116145 }
117146 m.image, cmd = m.image.Update(msg)
@@ -185,3 +214,16 @@ func (m *model) Position() (int, int) {
185214 col -= m.width / 2
186215 return row, col
187216}
217+
218+func ValidateFileSize(filePath string, sizeLimit int64) (bool, error) {
219+ fileInfo, err := os.Stat(filePath)
220+ if err != nil {
221+ return false, fmt.Errorf("error getting file info: %w", err)
222+ }
223+
224+ if fileInfo.Size() > sizeLimit {
225+ return true, nil
226+ }
227+
228+ return false, nil
229+}
internal/tui/page/chat/chat.go+11−1
@@ -6,6 +6,8 @@ import (
66 "github.com/charmbracelet/bubbles/v2/key"
77 tea "github.com/charmbracelet/bubbletea/v2"
88 "github.com/charmbracelet/crush/internal/app"
9+ "github.com/charmbracelet/crush/internal/config"
10+ "github.com/charmbracelet/crush/internal/llm/models"
911 "github.com/charmbracelet/crush/internal/logging"
1012 "github.com/charmbracelet/crush/internal/message"
1113 "github.com/charmbracelet/crush/internal/session"
@@ -87,7 +89,15 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8789 )
8890
8991 case key.Matches(msg, p.keyMap.FilePicker):
90− return p, util.CmdHandler(OpenFilePickerMsg{})
92+ cfg := config.Get()
93+ agentCfg := cfg.Agents[config.AgentCoder]
94+ selectedModelID := agentCfg.Model
95+ model := models.SupportedModels[selectedModelID]
96+ if model.SupportsAttachments {
97+ return p, util.CmdHandler(OpenFilePickerMsg{})
98+ } else {
99+ return p, util.ReportWarn("File attachments are not supported by the current model: " + string(selectedModelID))
100+ }
91101 case key.Matches(msg, p.keyMap.Tab):
92102 logging.Info("Tab key pressed, toggling chat focus")
93103 if p.session.ID == "" {
internal/tui/tui.go+1−2
@@ -85,7 +85,6 @@ func (a appModel) Init() tea.Cmd {
8585
8686// Update handles incoming messages and updates the application state.
8787func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
88− logging.Info("TUI Update", "msg", msg)
8988 var cmds []tea.Cmd
9089 var cmd tea.Cmd
9190
@@ -248,7 +247,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
248247// handleWindowResize processes window resize events and updates all components.
249248func (a *appModel) handleWindowResize(msg tea.WindowSizeMsg) tea.Cmd {
250249 var cmds []tea.Cmd
251− msg.Height -= 1 // Make space for the status bar
250+ msg.Height -= 2 // Make space for the status bar
252251 a.width, a.height = msg.Width, msg.Height
253252
254253 // Update status bar