From 6a81cb727a67619772b6d9d193eac89cf889cef5 Mon Sep 17 00:00:00 2001 From: Joe Beda Date: Wed, 19 Aug 2026 17:31:22 -0700 Subject: [PATCH] fix(mecatui): restore pasted newlines under modifyOtherKeys Co-Authored-By: mecatl --- cmd/mecatui/ui/paste_test.go | 11 +++++++++++ cmd/mecatui/ui/update.go | 16 +++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/cmd/mecatui/ui/paste_test.go b/cmd/mecatui/ui/paste_test.go index 7e3adfd978..6e15181318 100644 --- a/cmd/mecatui/ui/paste_test.go +++ b/cmd/mecatui/ui/paste_test.go @@ -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. diff --git a/cmd/mecatui/ui/update.go b/cmd/mecatui/ui/update.go index 7c852ff72c..2937d57806 100644 --- a/cmd/mecatui/ui/update.go +++ b/cmd/mecatui/ui/update.go @@ -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 @@ -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