Просмотр исходного кода

feat(tui): paste minimizing (#784)

Co-authored-by: adamdotdevin <[email protected]>
plyght 7 месяцев назад
Родитель
Сommit
4b2e52c834

+ 23 - 1
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{

+ 12 - 0
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" {

+ 75 - 5
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

+ 12 - 8
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)
 	}