Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions cmd/mecatui/ui/paste_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,17 @@ func TestPasteLandsInInputWhenIdle(t *testing.T) {
}
}

func TestPasteRestoresModifyOtherKeysNewlines(t *testing.T) {
m := zeroStateModel(t, embeddedCaps())

mm, _ := m.Update(pasteMsg("first" + xtermModifyOtherKeysCtrlJ + "second"))
m = mm.(Model)

if got := m.ta.Value(); got != "first\nsecond" {
t.Fatalf("input value = %q, want decoded newline", got)
}
}

// TestPasteLandsInInputWhileRunning: a paste mid-run lands in the textarea (the
// input stays focused for compose/enqueue while running) WITHOUT enqueuing —
// there is no enter, so the phase stays running and nothing is staged.
Expand Down
16 changes: 13 additions & 3 deletions cmd/mecatui/ui/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -1821,12 +1821,13 @@ func (m Model) onPaste(msg tea.PasteMsg) (tea.Model, tea.Cmd) {
if !m.pasteGateOpen() {
return m, nil
}
content := normalizePastedNewlines(msg.Content)
// A bracketed paste whose payload is a single media FILE PATH (the common
// drag-an-image-onto-the-terminal flow) is staged as an attachment instead of
// inserted literally. On ANY miss (not a path, not media, cap-gated, oversize)
// it returns ok=false and we fall through to the literal-text insert below
// (iteration-1 behaviour) — so prose pastes are completely unaffected.
if mm, cmd, ok := m.tryPasteMediaPath(msg.Content); ok {
if mm, cmd, ok := m.tryPasteMediaPath(content); ok {
return mm, cmd
}
// A LARGE text paste (>= pasteCharThreshold runes or >= pasteLineThreshold
Expand All @@ -1836,14 +1837,23 @@ func (m Model) onPaste(msg tea.PasteMsg) (tea.Model, tea.Cmd) {
// huge buffered paste makes every subsequent keypress O(paste) (issue #45).
// The full text expands back in place at submit/enqueue. Below the thresholds
// the literal insert below is byte-identical to the pre-staging behaviour.
if pasteNeedsStaging(m.ta.Value(), msg.Content) {
return m.stageLargePaste(msg.Content)
if pasteNeedsStaging(m.ta.Value(), content) {
return m.stageLargePaste(content)
}
msg.Content = content
var cmd tea.Cmd
m.ta, cmd = m.ta.Update(msg)
return m.afterInputEdit(cmd)
}

const xtermModifyOtherKeysCtrlJ = "\x1b[27;5;106~"

// normalizePastedNewlines restores newlines that xterm modifyOtherKeys encodes
// as Ctrl-J while bracketed paste is active.
func normalizePastedNewlines(content string) string {
return strings.ReplaceAll(content, xtermModifyOtherKeysCtrlJ, "\n")
}

// pasteGateOpen reports whether pasted text may reach the prompt input right now:
// no overlay/help/approval owns the screen, and the phase accepts input (idle or
// running — both keep the textarea focused for compose/enqueue). It is THE paste
Expand Down
Loading