Skip to content

Commit a4e7fa1

Browse files
committed
Fix multi-line inline editing of past user messages
The textarea's internal viewport was scrolling due to an incorrect height calculation that used simple character division instead of matching the textarea's word-aware wrapping. This caused ctrl-e, ctrl-a, ctrl-w to operate on wrong lines, and typing to go to unexpected positions. Fix by setting the textarea height to a generous value (the messages panel height) so the internal viewport never scrolls, then trimming end-of-buffer padding lines from the rendered output. This eliminates the height calculation entirely, along with the fragile cursor save/restore workaround. Assisted-By: cagent
1 parent 451540c commit a4e7fa1

1 file changed

Lines changed: 25 additions & 58 deletions

File tree

pkg/tui/components/messages/messages.go

Lines changed: 25 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,6 @@ func (m *model) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
246246
// Insert paste content into the inline edit textarea
247247
if m.inlineEditMsgIndex >= 0 {
248248
m.inlineEditTextarea.InsertString(msg.Content)
249-
m.updateInlineEditTextareaHeight()
250249
m.invalidateItem(m.inlineEditMsgIndex)
251250
m.renderDirty = true
252251
}
@@ -388,7 +387,6 @@ func (m *model) handleKeyPress(msg tea.KeyPressMsg) (layout.Model, tea.Cmd) {
388387
// Forward to textarea for newline insertion
389388
var cmd tea.Cmd
390389
m.inlineEditTextarea, cmd = m.inlineEditTextarea.Update(msg)
391-
m.updateInlineEditTextareaHeight()
392390
m.invalidateItem(m.inlineEditMsgIndex)
393391
m.renderDirty = true
394392
return m, cmd
@@ -407,7 +405,6 @@ func (m *model) handleKeyPress(msg tea.KeyPressMsg) (layout.Model, tea.Cmd) {
407405
// Forward all other keys to the textarea
408406
var cmd tea.Cmd
409407
m.inlineEditTextarea, cmd = m.inlineEditTextarea.Update(msg)
410-
m.updateInlineEditTextareaHeight()
411408
m.invalidateItem(m.inlineEditMsgIndex)
412409
m.renderDirty = true
413410
return m, cmd
@@ -964,55 +961,38 @@ func (m *model) renderInlineEditTextarea() string {
964961
m.inlineEditTextarea.SetWidth(innerWidth)
965962
}
966963

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+
967970
// Add a minimal edit indicator at the bottom left with extra padding
968971
editHint := styles.MutedStyle.Render("[editing]")
969972

970-
content := m.inlineEditTextarea.View() + "\n\n" + editHint
973+
content := view + "\n\n" + editHint
971974
return editStyle.Width(m.contentWidth()).Render(content)
972975
}
973976

974-
// updateInlineEditTextareaHeight recalculates and sets the textarea height based on current content.
975-
func (m *model) updateInlineEditTextareaHeight() {
976-
if m.inlineEditMsgIndex < 0 {
977-
return
978-
}
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")
979983

980-
editStyle := styles.UserMessageStyle
981-
innerWidth := m.contentWidth() - editStyle.GetHorizontalFrameSize()
982-
if innerWidth <= 0 {
983-
return
984-
}
985-
986-
content := m.inlineEditTextarea.Value()
987-
lineCount := 0
988-
for line := range strings.SplitSeq(content, "\n") {
989-
lineWidth := ansi.StringWidth(line)
990-
if lineWidth == 0 {
991-
lineCount++
992-
} else {
993-
lineCount += (lineWidth + innerWidth - 1) / innerWidth
994-
}
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+
last := len(lines)
987+
for last > 0 && strings.TrimSpace(ansi.Strip(lines[last-1])) == "" {
988+
last--
995989
}
996990

997-
newHeight := max(1, lineCount)
998-
if m.inlineEditTextarea.Height() == newHeight {
999-
return
991+
if last == 0 {
992+
return view
1000993
}
1001994

1002-
// Save cursor position
1003-
cursorRow := m.inlineEditTextarea.Line()
1004-
cursorCol := m.inlineEditTextarea.LineInfo().ColumnOffset
1005-
1006-
m.inlineEditTextarea.SetHeight(newHeight)
1007-
1008-
// Reset viewport scroll state by moving to start then restoring position
1009-
// NOTE(krissetto): This is a workaround because the textarea's internal viewport
1010-
// scrolling is not updated when the height is changed.
1011-
m.inlineEditTextarea.MoveToBegin()
1012-
for range cursorRow {
1013-
m.inlineEditTextarea.CursorDown()
1014-
}
1015-
m.inlineEditTextarea.SetCursorColumn(cursorCol)
995+
return strings.Join(lines[:last], "\n")
1016996
}
1017997

1018998
func (m *model) needsSeparator(index int) bool {
@@ -1704,23 +1684,10 @@ func (m *model) StartInlineEdit(msgIndex, sessionPosition int, content string) t
17041684
ta.SetWidth(innerWidth)
17051685
}
17061686

1707-
// Calculate appropriate height based on content
1708-
// Count lines and account for word wrapping
1709-
lineCount := 0
1710-
if innerWidth > 0 {
1711-
for line := range strings.SplitSeq(content, "\n") {
1712-
lineWidth := ansi.StringWidth(line)
1713-
if lineWidth == 0 {
1714-
// Empty line counts as 1 line
1715-
lineCount++
1716-
} else {
1717-
// Account for word wrapping: ceil(lineWidth / innerWidth)
1718-
lineCount += (lineWidth + innerWidth - 1) / innerWidth
1719-
}
1720-
}
1721-
}
1722-
// Set height to match content (minimum 1 line)
1723-
ta.SetHeight(max(1, lineCount))
1687+
// Set a generous height so the textarea's internal viewport never scrolls.
1688+
// This prevents cursor positioning bugs with multi-line content. The actual
1689+
// rendered output is trimmed in renderInlineEditTextarea to remove padding.
1690+
ta.SetHeight(max(1, m.height))
17241691

17251692
// Remove the default prompt/placeholder styling for a cleaner look
17261693
ta.Prompt = ""

0 commit comments

Comments
 (0)