@@ -242,6 +242,15 @@ func (m *model) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
242242 }
243243 // Fall through to forward tick to all views
244244
245+ case tea.PasteMsg :
246+ // Insert paste content into the inline edit textarea
247+ if m .inlineEditMsgIndex >= 0 {
248+ m .inlineEditTextarea .InsertString (msg .Content )
249+ m .invalidateItem (m .inlineEditMsgIndex )
250+ m .renderDirty = true
251+ }
252+ return m , nil
253+
245254 case tea.KeyPressMsg :
246255 return m .handleKeyPress (msg )
247256 }
@@ -378,7 +387,6 @@ func (m *model) handleKeyPress(msg tea.KeyPressMsg) (layout.Model, tea.Cmd) {
378387 // Forward to textarea for newline insertion
379388 var cmd tea.Cmd
380389 m .inlineEditTextarea , cmd = m .inlineEditTextarea .Update (msg )
381- m .updateInlineEditTextareaHeight ()
382390 m .invalidateItem (m .inlineEditMsgIndex )
383391 m .renderDirty = true
384392 return m , cmd
@@ -397,7 +405,6 @@ func (m *model) handleKeyPress(msg tea.KeyPressMsg) (layout.Model, tea.Cmd) {
397405 // Forward all other keys to the textarea
398406 var cmd tea.Cmd
399407 m .inlineEditTextarea , cmd = m .inlineEditTextarea .Update (msg )
400- m .updateInlineEditTextareaHeight ()
401408 m .invalidateItem (m .inlineEditMsgIndex )
402409 m .renderDirty = true
403410 return m , cmd
@@ -954,55 +961,36 @@ func (m *model) renderInlineEditTextarea() string {
954961 m .inlineEditTextarea .SetWidth (innerWidth )
955962 }
956963
964+ // The textarea is set to a large height to prevent internal viewport scrolling
965+ // which causes cursor positioning bugs in multi-line content. We trim the
966+ // end-of-buffer padding lines from the rendered output.
967+ view := m .inlineEditTextarea .View ()
968+ view = trimEndOfBufferLines (view )
969+
957970 // Add a minimal edit indicator at the bottom left with extra padding
958971 editHint := styles .MutedStyle .Render ("[editing]" )
959972
960- content := m . inlineEditTextarea . View () + "\n \n " + editHint
973+ content := view + "\n \n " + editHint
961974 return editStyle .Width (m .contentWidth ()).Render (content )
962975}
963976
964- // updateInlineEditTextareaHeight recalculates and sets the textarea height based on current content.
965- func (m * model ) updateInlineEditTextareaHeight () {
966- if m .inlineEditMsgIndex < 0 {
967- return
968- }
977+ // trimEndOfBufferLines removes trailing end-of-buffer padding lines from a
978+ // textarea's rendered View output. The textarea pads its view to fill its
979+ // configured height; these padding lines contain only whitespace (after
980+ // stripping ANSI sequences) and appear after the actual content.
981+ func trimEndOfBufferLines (view string ) string {
982+ lines := strings .Split (view , "\n " )
969983
970- editStyle := styles .UserMessageStyle
971- innerWidth := m .contentWidth () - editStyle .GetHorizontalFrameSize ()
972- if innerWidth <= 0 {
973- return
984+ // Trim trailing lines that are visually empty (whitespace-only after ANSI strip).
985+ // Content lines always contain visible text or cursor escape sequences.
986+ // Always keep at least one line so that an empty textarea still renders
987+ // the cursor line instead of returning the full padded view.
988+ last := len (lines )
989+ for last > 1 && strings .TrimSpace (ansi .Strip (lines [last - 1 ])) == "" {
990+ last --
974991 }
975992
976- content := m .inlineEditTextarea .Value ()
977- lineCount := 0
978- for line := range strings .SplitSeq (content , "\n " ) {
979- lineWidth := ansi .StringWidth (line )
980- if lineWidth == 0 {
981- lineCount ++
982- } else {
983- lineCount += (lineWidth + innerWidth - 1 ) / innerWidth
984- }
985- }
986-
987- newHeight := max (1 , lineCount )
988- if m .inlineEditTextarea .Height () == newHeight {
989- return
990- }
991-
992- // Save cursor position
993- cursorRow := m .inlineEditTextarea .Line ()
994- cursorCol := m .inlineEditTextarea .LineInfo ().ColumnOffset
995-
996- m .inlineEditTextarea .SetHeight (newHeight )
997-
998- // Reset viewport scroll state by moving to start then restoring position
999- // NOTE(krissetto): This is a workaround because the textarea's internal viewport
1000- // scrolling is not updated when the height is changed.
1001- m .inlineEditTextarea .MoveToBegin ()
1002- for range cursorRow {
1003- m .inlineEditTextarea .CursorDown ()
1004- }
1005- m .inlineEditTextarea .SetCursorColumn (cursorCol )
993+ return strings .Join (lines [:last ], "\n " )
1006994}
1007995
1008996func (m * model ) needsSeparator (index int ) bool {
@@ -1694,23 +1682,10 @@ func (m *model) StartInlineEdit(msgIndex, sessionPosition int, content string) t
16941682 ta .SetWidth (innerWidth )
16951683 }
16961684
1697- // Calculate appropriate height based on content
1698- // Count lines and account for word wrapping
1699- lineCount := 0
1700- if innerWidth > 0 {
1701- for line := range strings .SplitSeq (content , "\n " ) {
1702- lineWidth := ansi .StringWidth (line )
1703- if lineWidth == 0 {
1704- // Empty line counts as 1 line
1705- lineCount ++
1706- } else {
1707- // Account for word wrapping: ceil(lineWidth / innerWidth)
1708- lineCount += (lineWidth + innerWidth - 1 ) / innerWidth
1709- }
1710- }
1711- }
1712- // Set height to match content (minimum 1 line)
1713- ta .SetHeight (max (1 , lineCount ))
1685+ // Set a generous height so the textarea's internal viewport never scrolls.
1686+ // This prevents cursor positioning bugs with multi-line content. The actual
1687+ // rendered output is trimmed in renderInlineEditTextarea to remove padding.
1688+ ta .SetHeight (max (1 , m .height ))
17141689
17151690 // Remove the default prompt/placeholder styling for a cleaner look
17161691 ta .Prompt = ""
0 commit comments