diff --git a/internal/tui/workspace/views/todos_test.go b/internal/tui/workspace/views/todos_test.go index 2c7070506..a876b14dc 100644 --- a/internal/tui/workspace/views/todos_test.go +++ b/internal/tui/workspace/views/todos_test.go @@ -983,3 +983,19 @@ func newTextInputWithValue(val string) textinput.Model { ti.SetValue(val) return ti } + +// --- Edit description: multi-line content survives the composer --- + +func TestTodos_EditDescription_PreservesMultilineMarkdown(t *testing.T) { + v := testTodosViewWithTodos() + v.descComposer = widget.NewComposer(v.styles, widget.WithMode(widget.ComposerRich)) + + todos := sampleTodos() + todos[0].Description = "

para one

\n


\n

para two

" + v.session.Hub().Todos(42, 10).Set(todos) + + cmd := v.startEditDescription() + require.NotNil(t, cmd) + assert.Equal(t, "para one\n\npara two", v.descComposer.Value(), + "Reset must not drop the rich composer to single-line mode and flatten the description") +} diff --git a/internal/tui/workspace/widget/composer.go b/internal/tui/workspace/widget/composer.go index 2b4a860d4..51afaa834 100644 --- a/internal/tui/workspace/widget/composer.go +++ b/internal/tui/workspace/widget/composer.go @@ -114,10 +114,11 @@ func defaultComposerKeyMap() composerKeyMap { // Composer is a reusable Markdown editing widget with attachment support. type Composer struct { // Input widgets - textInput textinput.Model - textArea textarea.Model - mode ComposerMode - autoExpand bool // auto-switch quick→rich on markdown formatting + textInput textinput.Model + textArea textarea.Model + mode ComposerMode + initialMode ComposerMode // mode the composer was constructed with; Reset restores it + autoExpand bool // auto-switch quick→rich on markdown formatting // Attachments attachments []Attachment @@ -201,6 +202,7 @@ func NewComposer(styles *tui.Styles, opts ...ComposerOption) *Composer { for _, opt := range opts { opt(c) } + c.initialMode = c.mode return c } @@ -252,8 +254,13 @@ func (c *Composer) Value() string { return c.textArea.Value() } -// SetValue sets the text content (useful for pre-populating). +// SetValue sets the text content (useful for pre-populating). Multi-line or +// Markdown content expands a quick composer to rich mode first: the +// single-line textinput would silently flatten newlines. func (c *Composer) SetValue(s string) { + if c.mode == ComposerQuick && (strings.Contains(s, "\n") || richtext.IsMarkdown(s)) { + c.expandToRich() + } if c.mode == ComposerQuick { c.textInput.SetValue(s) } else { @@ -262,19 +269,20 @@ func (c *Composer) SetValue(s string) { } // InsertPaste appends pasted text at the current cursor position. -// If the text contains newlines or markdown, the composer auto-expands to rich mode. +// If the text contains newlines or markdown, SetValue auto-expands the +// composer to rich mode. func (c *Composer) InsertPaste(text string) { if text == "" { return } - if c.mode == ComposerQuick && (strings.Contains(text, "\n") || richtext.IsMarkdown(text)) { - c.expandToRich() - } - existing := c.Value() - c.SetValue(existing + text) + c.SetValue(c.Value() + text) } -// Reset clears all content, attachments, and returns to quick mode. +// Reset clears all content and attachments and returns the composer to its +// constructed mode. A quick composer that auto-expanded collapses back to +// quick; a composer built rich stays rich — dropping it to a single-line +// input would flatten the next multi-line SetValue and demote Enter from +// newline to send. func (c *Composer) Reset() { c.textInput.Reset() c.textArea.Reset() @@ -282,7 +290,7 @@ func (c *Composer) Reset() { c.attachCursor = -1 c.uploading = 0 c.preview = false - c.mode = ComposerQuick + c.mode = c.initialMode } // Attachments returns the current attachment list. @@ -743,15 +751,9 @@ func (c *Composer) HandleEditorReturn(msg EditorReturnMsg) tea.Cmd { if content == "" { return nil } - // If content has multiple lines or markdown, switch to rich mode - if strings.Contains(content, "\n") || richtext.IsMarkdown(content) { - if c.mode == ComposerQuick { - c.expandToRich() - } - c.textArea.SetValue(content) - } else { - c.SetValue(content) - } + // SetValue switches to rich mode when the content has multiple lines or + // markdown. + c.SetValue(content) return nil } diff --git a/internal/tui/workspace/widget/composer_test.go b/internal/tui/workspace/widget/composer_test.go index d97400ffd..92e35dfc0 100644 --- a/internal/tui/workspace/widget/composer_test.go +++ b/internal/tui/workspace/widget/composer_test.go @@ -57,12 +57,53 @@ func TestComposerReset(t *testing.T) { } } -func TestComposerResetReturnsToQuickMode(t *testing.T) { - c := NewComposer(testStyles(), WithMode(ComposerRich)) - c.SetValue("some text") - c.Reset() - if c.Mode() != ComposerQuick { - t.Errorf("mode after Reset = %d, want ComposerQuick", c.Mode()) +func TestComposerResetRestoresConstructedMode(t *testing.T) { + // A composer built rich stays rich across Reset: dropping to the + // single-line quick input would flatten the next multi-line SetValue + // (e.g. re-editing a to-do description) and demote Enter to send. + rich := NewComposer(testStyles(), WithMode(ComposerRich)) + rich.SetValue("some text") + rich.Reset() + if rich.Mode() != ComposerRich { + t.Errorf("mode after Reset = %d, want ComposerRich", rich.Mode()) + } + + // A quick composer that auto-expanded collapses back to quick. + quick := NewComposer(testStyles()) + quick.InsertPaste("line1\nline2") + if quick.Mode() != ComposerRich { + t.Fatalf("paste should have expanded to rich, got %d", quick.Mode()) + } + quick.Reset() + if quick.Mode() != ComposerQuick { + t.Errorf("mode after Reset = %d, want ComposerQuick", quick.Mode()) + } +} + +func TestComposerSetValueExpandsForMultilineContent(t *testing.T) { + // SetValue is a content entry point like InsertPaste and editor return: + // multi-line or Markdown content must expand a quick composer, or the + // single-line textinput silently flattens the newlines. + c := NewComposer(testStyles()) + c.SetValue("para one\n\npara two") + if c.Mode() != ComposerRich { + t.Errorf("mode after multi-line SetValue = %d, want ComposerRich", c.Mode()) + } + if c.Value() != "para one\n\npara two" { + t.Errorf("value = %q, newlines were flattened", c.Value()) + } + + // Single-line Markdown expands too — rendering it needs the rich editor. + markdown := NewComposer(testStyles()) + markdown.SetValue("some **bold** text") + if markdown.Mode() != ComposerRich { + t.Errorf("mode after Markdown SetValue = %d, want ComposerRich", markdown.Mode()) + } + + single := NewComposer(testStyles()) + single.SetValue("hello") + if single.Mode() != ComposerQuick { + t.Errorf("single-line SetValue should stay quick, got %d", single.Mode()) } }