on main help sign in
feat: allow to send the prompt if its empty but has text attachments (#1806)
617f9e2Carlos Alexandro Becker
internal/agent/agent.go+1−1
@@ -134,7 +134,7 @@ func NewSessionAgent(
134}
134}
135
135
136func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy.AgentResult, error) {
136func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy.AgentResult, error) {
137− if call.Prompt == "" {
137+ if call.Prompt == "" && !message.ContainsTextAttachment(call.Attachments) {
138 return nil, ErrEmptyPrompt
138 return nil, ErrEmptyPrompt
139 }
139 }
140 if call.SessionID == "" {
140 if call.SessionID == "" {
internal/message/attachment.go+11−1
@@ -1,6 +1,9 @@
1package message
1package message
2
2
3+import (
4+ "slices"
3−import "strings"
5+ "strings"
6+)
4
7
5type Attachment struct {
8type Attachment struct {
6 FilePath string
9 FilePath string
@@ -11,3 +14,10 @@ type Attachment struct {
11
14
12func (a Attachment) IsText() bool { return strings.HasPrefix(a.MimeType, "text/") }
15func (a Attachment) IsText() bool { return strings.HasPrefix(a.MimeType, "text/") }
13func (a Attachment) IsImage() bool { return strings.HasPrefix(a.MimeType, "image/") }
16func (a Attachment) IsImage() bool { return strings.HasPrefix(a.MimeType, "image/") }
17+
18+// ContainsTextAttachment returns true if any of the attachments is a text attachments.
19+func ContainsTextAttachment(attachments []Attachment) bool {
20+ return slices.ContainsFunc(attachments, func(a Attachment) bool {
21+ return a.IsText()
22+ })
23+}
internal/tui/components/chat/editor/editor.go+39−33
@@ -1,13 +1,14 @@
1package editor
1package editor
2
2
3import (
3import (
4− "errors"
5 "fmt"
4 "fmt"
6 "math/rand"
5 "math/rand"
7 "net/http"
6 "net/http"
8 "os"
7 "os"
9 "path/filepath"
8 "path/filepath"
9+ "regexp"
10 "slices"
10 "slices"
11+ "strconv"
11 "strings"
12 "strings"
12 "unicode"
13 "unicode"
13
14
@@ -146,7 +147,7 @@ func (m *editorCmp) send() tea.Cmd {
146
147
147 attachments := m.attachments
148 attachments := m.attachments
148
149
149− if value == "" {
150+ if value == "" && !message.ContainsTextAttachment(attachments) {
150 return nil
151 return nil
151 }
152 }
152
153
@@ -233,13 +234,31 @@ func (m *editorCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
233 m.textarea.SetValue(msg.Text)
234 m.textarea.SetValue(msg.Text)
234 m.textarea.MoveToEnd()
235 m.textarea.MoveToEnd()
235 case tea.PasteMsg:
236 case tea.PasteMsg:
236− content, path, err := pasteToFile(msg)
237+ // If pasted text has more than 2 newlines, treat it as a file attachment.
237− if errors.Is(err, errNotAFile) {
238+ if strings.Count(msg.Content, "\n") > 2 {
238− m.textarea, cmd = m.textarea.Update(msg)
239+ content := []byte(msg.Content)
239− return m, cmd
240+ if len(content) > maxAttachmentSize {
241+ return m, util.ReportWarn("Paste is too big (>5mb)")
242+ }
243+ name := fmt.Sprintf("paste_%d.txt", m.pasteIdx())
244+ mimeType := mimeOf(content)
245+ attachment := message.Attachment{
246+ FileName: name,
247+ FilePath: name,
248+ MimeType: mimeType,
249+ Content: content,
250+ }
251+ return m, util.CmdHandler(filepicker.FilePickedMsg{
252+ Attachment: attachment,
253+ })
240 }
254 }
255+
256+ // Try to parse as a file path.
257+ content, path, err := filepathToFile(msg.Content)
241 if err != nil {
258 if err != nil {
242− return m, util.ReportError(err)
259+ // Not a file path, just update the textarea normally.
260+ m.textarea, cmd = m.textarea.Update(msg)
261+ return m, cmd
243 }
262 }
244
263
245 if len(content) > maxAttachmentSize {
264 if len(content) > maxAttachmentSize {
@@ -256,7 +275,6 @@ func (m *editorCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
256 if !attachment.IsText() && !attachment.IsImage() {
275 if !attachment.IsText() && !attachment.IsImage() {
257 return m, util.ReportWarn("Invalid file content type: " + mimeType)
276 return m, util.ReportWarn("Invalid file content type: " + mimeType)
258 }
277 }
259− m.textarea.InsertString(attachment.FileName)
260 return m, util.CmdHandler(filepicker.FilePickedMsg{
278 return m, util.CmdHandler(filepicker.FilePickedMsg{
261 Attachment: attachment,
279 Attachment: attachment,
262 })
280 })
@@ -627,33 +645,21 @@ func New(app *app.App) Editor {
627
645
628var maxAttachmentSize = 5 * 1024 * 1024 // 5MB
646var maxAttachmentSize = 5 * 1024 * 1024 // 5MB
629
647
630−var errNotAFile = errors.New("not a file")
648+var pasteRE = regexp.MustCompile(`paste_(\d+).txt`)
631−
632−func pasteToFile(msg tea.PasteMsg) ([]byte, string, error) {
633− content, path, err := filepathToFile(msg.Content)
634− if err == nil {
635− return content, path, err
636− }
637−
638− if strings.Count(msg.Content, "\n") > 2 {
639− return contentToFile([]byte(msg.Content))
640− }
641−
642− return nil, "", errNotAFile
643−}
644
649
645−func contentToFile(content []byte) ([]byte, string, error) {
650+func (m *editorCmp) pasteIdx() int {
646− f, err := os.CreateTemp("", "paste_*.txt")
651+ result := 0
647− if err != nil {
652+ for _, at := range m.attachments {
648− return nil, "", err
653+ found := pasteRE.FindStringSubmatch(at.FileName)
649− }
654+ if len(found) == 0 {
650− if _, err := f.Write(content); err != nil {
655+ continue
651− return nil, "", err
652− }
656+ }
657+ idx, err := strconv.Atoi(found[1])
653− if err := f.Close(); err != nil {
658+ if err == nil {
654− return nil, "", err
659+ result = max(result, idx)
660+ }
655 }
661 }
656− return content, f.Name(), nil
662+ return result + 1
657}
663}
658
664
659func filepathToFile(name string) ([]byte, string, error) {
665func filepathToFile(name string) ([]byte, string, error) {
internal/tui/components/chat/messages/messages.go+5−3
@@ -223,8 +223,10 @@ func (m *messageCmp) renderAssistantMessage() string {
223// message content and any attached files with appropriate icons.
223// message content and any attached files with appropriate icons.
224func (m *messageCmp) renderUserMessage() string {
224func (m *messageCmp) renderUserMessage() string {
225 t := styles.CurrentTheme()
225 t := styles.CurrentTheme()
226− parts := []string{
226+ var parts []string
227+
227− m.toMarkdown(m.message.Content().String()),
228+ if s := m.message.Content().String(); s != "" {
229+ parts = append(parts, m.toMarkdown(s))
228 }
230 }
229
231
230 attachmentStyle := t.S().Base.
232 attachmentStyle := t.S().Base.
@@ -256,7 +258,7 @@ func (m *messageCmp) renderUserMessage() string {
256 }
258 }
257
259
258 if len(attachments) > 0 {
260 if len(attachments) > 0 {
259− parts = append(parts, "", strings.Join(attachments, ""))
261+ parts = append(parts, strings.Join(attachments, ""))
260 }
262 }
261
263
262 joined := lipgloss.JoinVertical(lipgloss.Left, parts...)
264 joined := lipgloss.JoinVertical(lipgloss.Left, parts...)