summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorplyght <[email protected]>2025-07-21 17:31:29 +0200
committerGitHub <[email protected]>2025-07-21 10:31:29 -0500
commit4b2e52c834680301556ad103f7cd885071b8df6e (patch)
tree6e8cac5cbde5a9590b3fef4cee6cfe28cfbf3922
parent6867658c0ff7b6d9d1d167ff8394c135b740877c (diff)
downloadopencode-4b2e52c834680301556ad103f7cd885071b8df6e.tar.gz
opencode-4b2e52c834680301556ad103f7cd885071b8df6e.zip
feat(tui): paste minimizing (#784)
Co-authored-by: adamdotdevin <[email protected]>
-rw-r--r--packages/tui/internal/app/prompt.go24
-rw-r--r--packages/tui/internal/attachment/attachment.go12
-rw-r--r--packages/tui/internal/components/chat/editor.go80
-rw-r--r--packages/tui/internal/components/chat/message.go20
4 files changed, 122 insertions, 14 deletions
diff --git a/packages/tui/internal/app/prompt.go b/packages/tui/internal/app/prompt.go
index dcd0b97fa..158951d84 100644
--- a/packages/tui/internal/app/prompt.go
+++ b/packages/tui/internal/app/prompt.go
@@ -25,12 +25,32 @@ func (p Prompt) ToMessage(
Created: float64(time.Now().UnixMilli()),
},
}
+
+ text := p.Text
+ textAttachments := []*attachment.Attachment{}
+ for _, attachment := range p.Attachments {
+ if attachment.Type == "text" {
+ textAttachments = append(textAttachments, attachment)
+ }
+ }
+ for i := 0; i < len(textAttachments)-1; i++ {
+ for j := i + 1; j < len(textAttachments); j++ {
+ if textAttachments[i].StartIndex < textAttachments[j].StartIndex {
+ textAttachments[i], textAttachments[j] = textAttachments[j], textAttachments[i]
+ }
+ }
+ }
+ for _, att := range textAttachments {
+ source, _ := att.GetTextSource()
+ text = text[:att.StartIndex] + source.Value + text[att.EndIndex:]
+ }
+
parts := []opencode.PartUnion{opencode.TextPart{
ID: id.Ascending(id.Part),
MessageID: messageID,
SessionID: sessionID,
Type: opencode.TextPartTypeText,
- Text: p.Text,
+ Text: text,
}}
for _, attachment := range p.Attachments {
text := opencode.FilePartSourceText{
@@ -40,6 +60,8 @@ func (p Prompt) ToMessage(
}
var source *opencode.FilePartSource
switch attachment.Type {
+ case "text":
+ continue
case "file":
fileSource, _ := attachment.GetFileSource()
source = &opencode.FilePartSource{
diff --git a/packages/tui/internal/attachment/attachment.go b/packages/tui/internal/attachment/attachment.go
index 178de9954..26bb91d49 100644
--- a/packages/tui/internal/attachment/attachment.go
+++ b/packages/tui/internal/attachment/attachment.go
@@ -4,6 +4,10 @@ import (
"github.com/google/uuid"
)
+type TextSource struct {
+ Value string `toml:"value"`
+}
+
type FileSource struct {
Path string `toml:"path"`
Mime string `toml:"mime"`
@@ -46,6 +50,14 @@ func NewAttachment() *Attachment {
}
}
+func (a *Attachment) GetTextSource() (*TextSource, bool) {
+ if a.Type != "text" {
+ return nil, false
+ }
+ ts, ok := a.Source.(*TextSource)
+ return ts, ok
+}
+
// GetFileSource returns the source as FileSource if the attachment is a file type
func (a *Attachment) GetFileSource() (*FileSource, bool) {
if a.Type != "file" {
diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go
index c05754cae..e4e62ed7e 100644
--- a/packages/tui/internal/components/chat/editor.go
+++ b/packages/tui/internal/components/chat/editor.go
@@ -56,6 +56,7 @@ type editorComponent struct {
exitKeyInDebounce bool
historyIndex int // -1 means current (not in history)
currentText string // Store current text when navigating history
+ pasteCounter int
}
func (m *editorComponent) Init() tea.Cmd {
@@ -129,12 +130,22 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
text, err := strconv.Unquote(`"` + text + `"`)
if err != nil {
slog.Error("Failed to unquote text", "error", err)
- m.textarea.InsertRunesFromUserInput([]rune(msg))
+ text := string(msg)
+ if m.shouldSummarizePastedText(text) {
+ m.handleLongPaste(text)
+ } else {
+ m.textarea.InsertRunesFromUserInput([]rune(msg))
+ }
return m, nil
}
if _, err := os.Stat(text); err != nil {
slog.Error("Failed to paste file", "error", err)
- m.textarea.InsertRunesFromUserInput([]rune(msg))
+ text := string(msg)
+ if m.shouldSummarizePastedText(text) {
+ m.handleLongPaste(text)
+ } else {
+ m.textarea.InsertRunesFromUserInput([]rune(msg))
+ }
return m, nil
}
@@ -142,7 +153,11 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
attachment := m.createAttachmentFromFile(filePath)
if attachment == nil {
- m.textarea.InsertRunesFromUserInput([]rune(msg))
+ if m.shouldSummarizePastedText(text) {
+ m.handleLongPaste(text)
+ } else {
+ m.textarea.InsertRunesFromUserInput([]rune(msg))
+ }
return m, nil
}
@@ -150,7 +165,12 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.textarea.InsertString(" ")
case tea.ClipboardMsg:
text := string(msg)
- m.textarea.InsertRunesFromUserInput([]rune(text))
+ // Check if the pasted text is long and should be summarized
+ if m.shouldSummarizePastedText(text) {
+ m.handleLongPaste(text)
+ } else {
+ m.textarea.InsertRunesFromUserInput([]rune(text))
+ }
case dialog.ThemeSelectedMsg:
m.textarea = updateTextareaStyles(m.textarea)
m.spinner = createSpinner()
@@ -392,6 +412,7 @@ func (m *editorComponent) Clear() (tea.Model, tea.Cmd) {
m.textarea.Reset()
m.historyIndex = -1
m.currentText = ""
+ m.pasteCounter = 0
return m, nil
}
@@ -421,7 +442,13 @@ func (m *editorComponent) Paste() (tea.Model, tea.Cmd) {
textBytes := clipboard.Read(clipboard.FmtText)
if textBytes != nil {
- m.textarea.InsertRunesFromUserInput([]rune(string(textBytes)))
+ text := string(textBytes)
+ // Check if the pasted text is long and should be summarized
+ if m.shouldSummarizePastedText(text) {
+ m.handleLongPaste(text)
+ } else {
+ m.textarea.InsertRunesFromUserInput([]rune(text))
+ }
return m, nil
}
@@ -490,6 +517,48 @@ func (m *editorComponent) getExitKeyText() string {
return m.app.Commands[commands.AppExitCommand].Keys()[0]
}
+// shouldSummarizePastedText determines if pasted text should be summarized
+func (m *editorComponent) shouldSummarizePastedText(text string) bool {
+ lines := strings.Split(text, "\n")
+ lineCount := len(lines)
+ charCount := len(text)
+
+ // Consider text long if it has more than 3 lines or more than 150 characters
+ return lineCount > 3 || charCount > 150
+}
+
+// handleLongPaste handles long pasted text by creating a summary attachment
+func (m *editorComponent) handleLongPaste(text string) {
+ lines := strings.Split(text, "\n")
+ lineCount := len(lines)
+
+ // Increment paste counter
+ m.pasteCounter++
+
+ // Create attachment with full text as base64 encoded data
+ fileBytes := []byte(text)
+ base64EncodedText := base64.StdEncoding.EncodeToString(fileBytes)
+ url := fmt.Sprintf("data:text/plain;base64,%s", base64EncodedText)
+
+ fileName := fmt.Sprintf("pasted-text-%d.txt", m.pasteCounter)
+ displayText := fmt.Sprintf("[pasted #%d %d+ lines]", m.pasteCounter, lineCount)
+
+ attachment := &attachment.Attachment{
+ ID: uuid.NewString(),
+ Type: "text",
+ MediaType: "text/plain",
+ Display: displayText,
+ URL: url,
+ Filename: fileName,
+ Source: &attachment.TextSource{
+ Value: text,
+ },
+ }
+
+ m.textarea.InsertAttachment(attachment)
+ m.textarea.InsertString(" ")
+}
+
func updateTextareaStyles(ta textarea.Model) textarea.Model {
t := theme.CurrentTheme()
bgColor := t.BackgroundElement()
@@ -551,6 +620,7 @@ func NewEditorComponent(app *app.App) EditorComponent {
spinner: s,
interruptKeyInDebounce: false,
historyIndex: -1,
+ pasteCounter: 0,
}
return m
diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go
index 31b345766..9f38ca8df 100644
--- a/packages/tui/internal/components/chat/message.go
+++ b/packages/tui/internal/components/chat/message.go
@@ -196,16 +196,20 @@ func renderText(
case opencode.UserMessage:
ts = time.UnixMilli(int64(casted.Time.Created))
base := styles.NewStyle().Foreground(t.Text()).Background(backgroundColor)
- words := strings.Fields(text)
- for i, word := range words {
- if strings.HasPrefix(word, "@") {
- words[i] = base.Foreground(t.Secondary()).Render(word + " ")
- } else {
- words[i] = base.Render(word + " ")
+ text = ansi.WordwrapWc(text, width-6, " -")
+ lines := strings.Split(text, "\n")
+ for i, line := range lines {
+ words := strings.Fields(line)
+ for i, word := range words {
+ if strings.HasPrefix(word, "@") {
+ words[i] = base.Foreground(t.Secondary()).Render(word + " ")
+ } else {
+ words[i] = base.Render(word + " ")
+ }
}
+ lines[i] = strings.Join(words, "")
}
- text = strings.Join(words, "")
- text = ansi.WordwrapWc(text, width-6, " -")
+ text = strings.Join(lines, "\n")
content = base.Width(width - 6).Render(text)
}