Skip to content
Open
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
16 changes: 16 additions & 0 deletions internal/tui/workspace/views/todos_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<p>para one</p>\n<p><br></p>\n<p>para two</p>"
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")
}
46 changes: 24 additions & 22 deletions internal/tui/workspace/widget/composer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -201,6 +202,7 @@ func NewComposer(styles *tui.Styles, opts ...ComposerOption) *Composer {
for _, opt := range opts {
opt(c)
}
c.initialMode = c.mode

return c
}
Expand Down Expand Up @@ -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 {
Expand All @@ -262,27 +269,28 @@ 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()
c.attachments = nil
c.attachCursor = -1
c.uploading = 0
c.preview = false
c.mode = ComposerQuick
c.mode = c.initialMode
}

// Attachments returns the current attachment list.
Expand Down Expand Up @@ -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
}

Expand Down
53 changes: 47 additions & 6 deletions internal/tui/workspace/widget/composer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}

Expand Down