summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorTimo Clasen <[email protected]>2025-07-18 11:47:20 +0200
committerGitHub <[email protected]>2025-07-18 04:47:20 -0500
commitdf33143396e126ca506822e9d3701fcbaebc8413 (patch)
tree262e5b6c2cbd54a0e663ce0dbfb74fe5e473e352
parent571aeaaea2f149f0ad4c7bb69f288e9f8ddd9897 (diff)
downloadopencode-df33143396e126ca506822e9d3701fcbaebc8413.tar.gz
opencode-df33143396e126ca506822e9d3701fcbaebc8413.zip
feat(tui): parse for file attachments when exiting EDITOR (#1117)
-rw-r--r--packages/tui/internal/components/chat/editor.go161
-rw-r--r--packages/tui/internal/tui/tui.go2
2 files changed, 103 insertions, 60 deletions
diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go
index efbc08349..085d1e9df 100644
--- a/packages/tui/internal/components/chat/editor.go
+++ b/packages/tui/internal/components/chat/editor.go
@@ -40,6 +40,7 @@ type EditorComponent interface {
Paste() (tea.Model, tea.Cmd)
Newline() (tea.Model, tea.Cmd)
SetValue(value string)
+ SetValueWithAttachments(value string)
SetInterruptKeyInDebounce(inDebounce bool)
SetExitKeyInDebounce(inDebounce bool)
}
@@ -94,51 +95,13 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
filePath := text
- ext := strings.ToLower(filepath.Ext(filePath))
-
- mediaType := ""
- switch ext {
- case ".jpg":
- mediaType = "image/jpeg"
- case ".png", ".jpeg", ".gif", ".webp":
- mediaType = "image/" + ext[1:]
- case ".pdf":
- mediaType = "application/pdf"
- default:
- attachment := &textarea.Attachment{
- ID: uuid.NewString(),
- Display: "@" + filePath,
- URL: fmt.Sprintf("file://./%s", filePath),
- Filename: filePath,
- MediaType: "text/plain",
- }
- m.textarea.InsertAttachment(attachment)
- m.textarea.InsertString(" ")
- return m, nil
- }
- fileBytes, err := os.ReadFile(filePath)
- if err != nil {
- slog.Error("Failed to read file", "error", err)
+ attachment := m.createAttachmentFromFile(filePath)
+ if attachment == nil {
m.textarea.InsertRunesFromUserInput([]rune(msg))
return m, nil
}
- base64EncodedFile := base64.StdEncoding.EncodeToString(fileBytes)
- url := fmt.Sprintf("data:%s;base64,%s", mediaType, base64EncodedFile)
- attachmentCount := len(m.textarea.GetAttachments())
- attachmentIndex := attachmentCount + 1
- label := "File"
- if strings.HasPrefix(mediaType, "image/") {
- label = "Image"
- }
- attachment := &textarea.Attachment{
- ID: uuid.NewString(),
- MediaType: mediaType,
- Display: fmt.Sprintf("[%s #%d]", label, attachmentIndex),
- URL: url,
- Filename: filePath,
- }
m.textarea.InsertAttachment(attachment)
m.textarea.InsertString(" ")
case tea.ClipboardMsg:
@@ -173,25 +136,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Now, insert the attachment at the position where the '@' was.
// The cursor is now at `atIndex` after the replacement.
filePath := msg.Item.Value
- extension := filepath.Ext(filePath)
- mediaType := ""
- switch extension {
- case ".jpg":
- mediaType = "image/jpeg"
- case ".png", ".jpeg", ".gif", ".webp":
- mediaType = "image/" + extension[1:]
- case ".pdf":
- mediaType = "application/pdf"
- default:
- mediaType = "text/plain"
- }
- attachment := &textarea.Attachment{
- ID: uuid.NewString(),
- Display: "@" + filePath,
- URL: fmt.Sprintf("file://./%s", url.PathEscape(filePath)),
- Filename: filePath,
- MediaType: mediaType,
- }
+ attachment := m.createAttachmentFromPath(filePath)
m.textarea.InsertAttachment(attachment)
m.textarea.InsertString(" ")
return m, nil
@@ -424,6 +369,38 @@ func (m *editorComponent) SetValue(value string) {
m.textarea.SetValue(value)
}
+func (m *editorComponent) SetValueWithAttachments(value string) {
+ m.textarea.Reset()
+
+ i := 0
+ for i < len(value) {
+ // Check if filepath and add attachment
+ if value[i] == '@' {
+ start := i + 1
+ end := start
+ for end < len(value) && value[end] != ' ' && value[end] != '\t' && value[end] != '\n' && value[end] != '\r' {
+ end++
+ }
+
+ if end > start {
+ filePath := value[start:end]
+ if _, err := os.Stat(filePath); err == nil {
+ attachment := m.createAttachmentFromFile(filePath)
+ if attachment != nil {
+ m.textarea.InsertAttachment(attachment)
+ i = end
+ continue
+ }
+ }
+ }
+ }
+
+ // Not a valid file path, insert the character normally
+ m.textarea.InsertRune(rune(value[i]))
+ i++
+ }
+}
+
func (m *editorComponent) SetExitKeyInDebounce(inDebounce bool) {
m.exitKeyInDebounce = inDebounce
}
@@ -504,3 +481,69 @@ func NewEditorComponent(app *app.App) EditorComponent {
return m
}
+
+func getMediaTypeFromExtension(ext string) string {
+ switch strings.ToLower(ext) {
+ case ".jpg":
+ return "image/jpeg"
+ case ".png", ".jpeg", ".gif", ".webp":
+ return "image/" + ext[1:]
+ case ".pdf":
+ return "application/pdf"
+ default:
+ return "text/plain"
+ }
+}
+
+func (m *editorComponent) createAttachmentFromFile(filePath string) *textarea.Attachment {
+ ext := strings.ToLower(filepath.Ext(filePath))
+ mediaType := getMediaTypeFromExtension(ext)
+
+ // For text files, create a simple file reference
+ if mediaType == "text/plain" {
+ return &textarea.Attachment{
+ ID: uuid.NewString(),
+ Display: "@" + filePath,
+ URL: fmt.Sprintf("file://./%s", filePath),
+ Filename: filePath,
+ MediaType: mediaType,
+ }
+ }
+
+ // For binary files (images, PDFs), read and encode
+ fileBytes, err := os.ReadFile(filePath)
+ if err != nil {
+ slog.Error("Failed to read file", "error", err)
+ return nil
+ }
+
+ base64EncodedFile := base64.StdEncoding.EncodeToString(fileBytes)
+ url := fmt.Sprintf("data:%s;base64,%s", mediaType, base64EncodedFile)
+ attachmentCount := len(m.textarea.GetAttachments())
+ attachmentIndex := attachmentCount + 1
+ label := "File"
+ if strings.HasPrefix(mediaType, "image/") {
+ label = "Image"
+ }
+
+ return &textarea.Attachment{
+ ID: uuid.NewString(),
+ MediaType: mediaType,
+ Display: fmt.Sprintf("[%s #%d]", label, attachmentIndex),
+ URL: url,
+ Filename: filePath,
+ }
+}
+
+func (m *editorComponent) createAttachmentFromPath(filePath string) *textarea.Attachment {
+ extension := filepath.Ext(filePath)
+ mediaType := getMediaTypeFromExtension(extension)
+
+ return &textarea.Attachment{
+ ID: uuid.NewString(),
+ Display: "@" + filePath,
+ URL: fmt.Sprintf("file://./%s", url.PathEscape(filePath)),
+ Filename: filePath,
+ MediaType: mediaType,
+ }
+}
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index e5b578bc2..b201a9086 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -334,7 +334,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds = append(cmds, cmd)
case app.SetEditorContentMsg:
// Set the editor content without sending
- a.editor.SetValue(msg.Text)
+ a.editor.SetValueWithAttachments(msg.Text)
updated, cmd := a.editor.Focus()
a.editor = updated.(chat.EditorComponent)
cmds = append(cmds, cmd)