Skip to content

Commit 3f92fed

Browse files
timoclsnnipuna-perera
authored andcommitted
feat(tui): parse for file attachments when exiting EDITOR (sst#1117)
1 parent 28b58c2 commit 3f92fed

File tree

2 files changed

+103
-60
lines changed

2 files changed

+103
-60
lines changed

packages/tui/internal/components/chat/editor.go

Lines changed: 102 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type EditorComponent interface {
4040
Paste() (tea.Model, tea.Cmd)
4141
Newline() (tea.Model, tea.Cmd)
4242
SetValue(value string)
43+
SetValueWithAttachments(value string)
4344
SetInterruptKeyInDebounce(inDebounce bool)
4445
SetExitKeyInDebounce(inDebounce bool)
4546
}
@@ -94,51 +95,13 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
9495
}
9596

9697
filePath := text
97-
ext := strings.ToLower(filepath.Ext(filePath))
98-
99-
mediaType := ""
100-
switch ext {
101-
case ".jpg":
102-
mediaType = "image/jpeg"
103-
case ".png", ".jpeg", ".gif", ".webp":
104-
mediaType = "image/" + ext[1:]
105-
case ".pdf":
106-
mediaType = "application/pdf"
107-
default:
108-
attachment := &textarea.Attachment{
109-
ID: uuid.NewString(),
110-
Display: "@" + filePath,
111-
URL: fmt.Sprintf("file://./%s", filePath),
112-
Filename: filePath,
113-
MediaType: "text/plain",
114-
}
115-
m.textarea.InsertAttachment(attachment)
116-
m.textarea.InsertString(" ")
117-
return m, nil
118-
}
11998

120-
fileBytes, err := os.ReadFile(filePath)
121-
if err != nil {
122-
slog.Error("Failed to read file", "error", err)
99+
attachment := m.createAttachmentFromFile(filePath)
100+
if attachment == nil {
123101
m.textarea.InsertRunesFromUserInput([]rune(msg))
124102
return m, nil
125103
}
126-
base64EncodedFile := base64.StdEncoding.EncodeToString(fileBytes)
127-
url := fmt.Sprintf("data:%s;base64,%s", mediaType, base64EncodedFile)
128-
attachmentCount := len(m.textarea.GetAttachments())
129-
attachmentIndex := attachmentCount + 1
130-
label := "File"
131-
if strings.HasPrefix(mediaType, "image/") {
132-
label = "Image"
133-
}
134104

135-
attachment := &textarea.Attachment{
136-
ID: uuid.NewString(),
137-
MediaType: mediaType,
138-
Display: fmt.Sprintf("[%s #%d]", label, attachmentIndex),
139-
URL: url,
140-
Filename: filePath,
141-
}
142105
m.textarea.InsertAttachment(attachment)
143106
m.textarea.InsertString(" ")
144107
case tea.ClipboardMsg:
@@ -173,25 +136,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
173136
// Now, insert the attachment at the position where the '@' was.
174137
// The cursor is now at `atIndex` after the replacement.
175138
filePath := msg.Item.Value
176-
extension := filepath.Ext(filePath)
177-
mediaType := ""
178-
switch extension {
179-
case ".jpg":
180-
mediaType = "image/jpeg"
181-
case ".png", ".jpeg", ".gif", ".webp":
182-
mediaType = "image/" + extension[1:]
183-
case ".pdf":
184-
mediaType = "application/pdf"
185-
default:
186-
mediaType = "text/plain"
187-
}
188-
attachment := &textarea.Attachment{
189-
ID: uuid.NewString(),
190-
Display: "@" + filePath,
191-
URL: fmt.Sprintf("file://./%s", url.PathEscape(filePath)),
192-
Filename: filePath,
193-
MediaType: mediaType,
194-
}
139+
attachment := m.createAttachmentFromPath(filePath)
195140
m.textarea.InsertAttachment(attachment)
196141
m.textarea.InsertString(" ")
197142
return m, nil
@@ -424,6 +369,38 @@ func (m *editorComponent) SetValue(value string) {
424369
m.textarea.SetValue(value)
425370
}
426371

372+
func (m *editorComponent) SetValueWithAttachments(value string) {
373+
m.textarea.Reset()
374+
375+
i := 0
376+
for i < len(value) {
377+
// Check if filepath and add attachment
378+
if value[i] == '@' {
379+
start := i + 1
380+
end := start
381+
for end < len(value) && value[end] != ' ' && value[end] != '\t' && value[end] != '\n' && value[end] != '\r' {
382+
end++
383+
}
384+
385+
if end > start {
386+
filePath := value[start:end]
387+
if _, err := os.Stat(filePath); err == nil {
388+
attachment := m.createAttachmentFromFile(filePath)
389+
if attachment != nil {
390+
m.textarea.InsertAttachment(attachment)
391+
i = end
392+
continue
393+
}
394+
}
395+
}
396+
}
397+
398+
// Not a valid file path, insert the character normally
399+
m.textarea.InsertRune(rune(value[i]))
400+
i++
401+
}
402+
}
403+
427404
func (m *editorComponent) SetExitKeyInDebounce(inDebounce bool) {
428405
m.exitKeyInDebounce = inDebounce
429406
}
@@ -504,3 +481,69 @@ func NewEditorComponent(app *app.App) EditorComponent {
504481

505482
return m
506483
}
484+
485+
func getMediaTypeFromExtension(ext string) string {
486+
switch strings.ToLower(ext) {
487+
case ".jpg":
488+
return "image/jpeg"
489+
case ".png", ".jpeg", ".gif", ".webp":
490+
return "image/" + ext[1:]
491+
case ".pdf":
492+
return "application/pdf"
493+
default:
494+
return "text/plain"
495+
}
496+
}
497+
498+
func (m *editorComponent) createAttachmentFromFile(filePath string) *textarea.Attachment {
499+
ext := strings.ToLower(filepath.Ext(filePath))
500+
mediaType := getMediaTypeFromExtension(ext)
501+
502+
// For text files, create a simple file reference
503+
if mediaType == "text/plain" {
504+
return &textarea.Attachment{
505+
ID: uuid.NewString(),
506+
Display: "@" + filePath,
507+
URL: fmt.Sprintf("file://./%s", filePath),
508+
Filename: filePath,
509+
MediaType: mediaType,
510+
}
511+
}
512+
513+
// For binary files (images, PDFs), read and encode
514+
fileBytes, err := os.ReadFile(filePath)
515+
if err != nil {
516+
slog.Error("Failed to read file", "error", err)
517+
return nil
518+
}
519+
520+
base64EncodedFile := base64.StdEncoding.EncodeToString(fileBytes)
521+
url := fmt.Sprintf("data:%s;base64,%s", mediaType, base64EncodedFile)
522+
attachmentCount := len(m.textarea.GetAttachments())
523+
attachmentIndex := attachmentCount + 1
524+
label := "File"
525+
if strings.HasPrefix(mediaType, "image/") {
526+
label = "Image"
527+
}
528+
529+
return &textarea.Attachment{
530+
ID: uuid.NewString(),
531+
MediaType: mediaType,
532+
Display: fmt.Sprintf("[%s #%d]", label, attachmentIndex),
533+
URL: url,
534+
Filename: filePath,
535+
}
536+
}
537+
538+
func (m *editorComponent) createAttachmentFromPath(filePath string) *textarea.Attachment {
539+
extension := filepath.Ext(filePath)
540+
mediaType := getMediaTypeFromExtension(extension)
541+
542+
return &textarea.Attachment{
543+
ID: uuid.NewString(),
544+
Display: "@" + filePath,
545+
URL: fmt.Sprintf("file://./%s", url.PathEscape(filePath)),
546+
Filename: filePath,
547+
MediaType: mediaType,
548+
}
549+
}

packages/tui/internal/tui/tui.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
334334
cmds = append(cmds, cmd)
335335
case app.SetEditorContentMsg:
336336
// Set the editor content without sending
337-
a.editor.SetValue(msg.Text)
337+
a.editor.SetValueWithAttachments(msg.Text)
338338
updated, cmd := a.editor.Focus()
339339
a.editor = updated.(chat.EditorComponent)
340340
cmds = append(cmds, cmd)

0 commit comments

Comments
 (0)