Detailed changes
@@ -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/dialogs/filepicker"
"github.com/charmbracelet/crush/internal/tui/layout"
"github.com/charmbracelet/crush/internal/tui/styles"
"github.com/charmbracelet/crush/internal/tui/util"
@@ -141,13 +142,13 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.session = msg
}
return m, nil
- // case dialog.AttachmentAddedMsg:
- // if len(m.attachments) >= maxAttachments {
- // logging.ErrorPersist(fmt.Sprintf("cannot add more than %d images", maxAttachments))
- // return m, cmd
- // }
- // m.attachments = append(m.attachments, msg.Attachment)
- // return m, nil
+ case filepicker.FilePickedMsg:
+ if len(m.attachments) >= maxAttachments {
+ logging.ErrorPersist(fmt.Sprintf("cannot add more than %d images", maxAttachments))
+ return m, cmd
+ }
+ m.attachments = append(m.attachments, msg.Attachment)
+ return m, nil
case completions.CompletionsClosedMsg:
m.isCompletionsOpen = false
m.currentQuery = ""
@@ -351,7 +352,24 @@ func (m *editorCmp) startCompletions() tea.Msg {
}
}
-func CreateTextArea(existing *textarea.Model) textarea.Model {
+// Blur implements Container.
+func (c *editorCmp) Blur() tea.Cmd {
+ c.textarea.Blur()
+ return nil
+}
+
+// Focus implements Container.
+func (c *editorCmp) Focus() tea.Cmd {
+ logging.Info("Focusing editor textarea")
+ return c.textarea.Focus()
+}
+
+// IsFocused implements Container.
+func (c *editorCmp) IsFocused() bool {
+ return c.textarea.Focused()
+}
+
+func NewEditorCmp(app *app.App) util.Model {
t := styles.CurrentTheme()
ta := textarea.New()
ta.SetStyles(t.S().TextArea)
@@ -369,36 +387,8 @@ func CreateTextArea(existing *textarea.Model) textarea.Model {
ta.CharLimit = -1
ta.Placeholder = "Tell me more about this project..."
ta.SetVirtualCursor(false)
-
- if existing != nil {
- ta.SetValue(existing.Value())
- ta.SetWidth(existing.Width())
- ta.SetHeight(existing.Height())
- }
-
ta.Focus()
- return ta
-}
-// Blur implements Container.
-func (c *editorCmp) Blur() tea.Cmd {
- c.textarea.Blur()
- return nil
-}
-
-// Focus implements Container.
-func (c *editorCmp) Focus() tea.Cmd {
- logging.Info("Focusing editor textarea")
- return c.textarea.Focus()
-}
-
-// IsFocused implements Container.
-func (c *editorCmp) IsFocused() bool {
- return c.textarea.Focused()
-}
-
-func NewEditorCmp(app *app.App) util.Model {
- ta := CreateTextArea(nil)
return &editorCmp{
app: app,
textarea: ta,
@@ -88,7 +88,7 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m statusCmp) View() tea.View {
t := styles.CurrentTheme()
- status := t.S().Base.Padding(0, 1).Render(m.help.View(DefaultKeyMap("focus chat")))
+ status := t.S().Base.Padding(0, 1, 1, 1).Render(m.help.View(DefaultKeyMap("focus chat")))
if m.info.Msg != "" {
switch m.info.Type {
case util.InfoTypeError:
@@ -1,13 +1,18 @@
package filepicker
import (
+ "fmt"
+ "net/http"
"os"
+ "path/filepath"
"strings"
"github.com/charmbracelet/bubbles/v2/filepicker"
"github.com/charmbracelet/bubbles/v2/help"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
+ "github.com/charmbracelet/crush/internal/logging"
+ "github.com/charmbracelet/crush/internal/message"
"github.com/charmbracelet/crush/internal/tui/components/core"
"github.com/charmbracelet/crush/internal/tui/components/dialogs"
"github.com/charmbracelet/crush/internal/tui/components/image"
@@ -23,7 +28,7 @@ const (
)
type FilePickedMsg struct {
- FilePath string
+ Attachment message.Attachment
}
type FilePicker interface {
@@ -111,7 +116,31 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Get the path of the selected file.
return m, tea.Sequence(
util.CmdHandler(dialogs.CloseDialogMsg{}),
- util.CmdHandler(FilePickedMsg{FilePath: path}),
+ func() tea.Msg {
+ isFileLarge, err := ValidateFileSize(path, maxAttachmentSize)
+ if err != nil {
+ logging.ErrorPersist("unable to read the image")
+ return nil
+ }
+ if isFileLarge {
+ logging.ErrorPersist("file too large, max 5MB")
+ return nil
+ }
+
+ content, err := os.ReadFile(path)
+ if err != nil {
+ logging.ErrorPersist("Unable read selected file")
+ return nil
+ }
+
+ mimeBufferSize := min(512, len(content))
+ mimeType := http.DetectContentType(content[:mimeBufferSize])
+ fileName := filepath.Base(path)
+ attachment := message.Attachment{FilePath: path, FileName: fileName, MimeType: mimeType, Content: content}
+ return FilePickedMsg{
+ Attachment: attachment,
+ }
+ },
)
}
m.image, cmd = m.image.Update(msg)
@@ -185,3 +214,16 @@ func (m *model) Position() (int, int) {
col -= m.width / 2
return row, col
}
+
+func ValidateFileSize(filePath string, sizeLimit int64) (bool, error) {
+ fileInfo, err := os.Stat(filePath)
+ if err != nil {
+ return false, fmt.Errorf("error getting file info: %w", err)
+ }
+
+ if fileInfo.Size() > sizeLimit {
+ return true, nil
+ }
+
+ return false, nil
+}
@@ -6,6 +6,8 @@ import (
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/crush/internal/app"
+ "github.com/charmbracelet/crush/internal/config"
+ "github.com/charmbracelet/crush/internal/llm/models"
"github.com/charmbracelet/crush/internal/logging"
"github.com/charmbracelet/crush/internal/message"
"github.com/charmbracelet/crush/internal/session"
@@ -87,7 +89,15 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
)
case key.Matches(msg, p.keyMap.FilePicker):
- return p, util.CmdHandler(OpenFilePickerMsg{})
+ cfg := config.Get()
+ agentCfg := cfg.Agents[config.AgentCoder]
+ selectedModelID := agentCfg.Model
+ model := models.SupportedModels[selectedModelID]
+ if model.SupportsAttachments {
+ return p, util.CmdHandler(OpenFilePickerMsg{})
+ } else {
+ return p, util.ReportWarn("File attachments are not supported by the current model: " + string(selectedModelID))
+ }
case key.Matches(msg, p.keyMap.Tab):
logging.Info("Tab key pressed, toggling chat focus")
if p.session.ID == "" {
@@ -85,7 +85,6 @@ func (a appModel) Init() tea.Cmd {
// Update handles incoming messages and updates the application state.
func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- logging.Info("TUI Update", "msg", msg)
var cmds []tea.Cmd
var cmd tea.Cmd
@@ -248,7 +247,7 @@ 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 {
var cmds []tea.Cmd
- msg.Height -= 1 // Make space for the status bar
+ msg.Height -= 2 // Make space for the status bar
a.width, a.height = msg.Width, msg.Height
// Update status bar