Add interactive Markdown checklist toggles in the ticket preview.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Skruecken
2026-10-19 14:37:14 +02:00
co-authored by Cursor
parent 7666715f6c
commit f08084d5e1
8 changed files with 366 additions and 21 deletions
+65 -11
View File
@@ -26,13 +26,14 @@ type TicketListStyles struct {
// TicketList is a custom viewport list of ticket cards.
type TicketList struct {
Items []ticket.Ticket
Cursor int
Offset int
Width int
Height int
Styles TicketListStyles
SpentByID map[string]time.Duration // optional; shown on selected card
Items []ticket.Ticket
Cursor int
Offset int
Width int
Height int
Styles TicketListStyles
SpentByID map[string]time.Duration // optional; shown on selected card
ChecklistCursor int // index into checkboxes of the selected ticket
}
// SetSize updates the viewport dimensions.
@@ -49,6 +50,7 @@ func (l *TicketList) SetItems(items []ticket.Ticket) {
l.Cursor = max(0, len(l.Items)-1)
}
l.Offset = 0
l.clampChecklistCursor()
l.ensureVisible()
}
@@ -70,6 +72,7 @@ func (l *TicketList) Len() int { return len(l.Items) }
func (l *TicketList) MoveDown() {
if l.Cursor < len(l.Items)-1 {
l.Cursor++
l.ChecklistCursor = 0
l.ensureVisible()
}
}
@@ -78,6 +81,7 @@ func (l *TicketList) MoveDown() {
func (l *TicketList) MoveUp() {
if l.Cursor > 0 {
l.Cursor--
l.ChecklistCursor = 0
l.ensureVisible()
}
}
@@ -87,10 +91,61 @@ func (l *TicketList) Select(idx int) {
if idx < 0 || idx >= len(l.Items) {
return
}
if l.Cursor != idx {
l.ChecklistCursor = 0
}
l.Cursor = idx
l.clampChecklistCursor()
l.ensureVisible()
}
// ChecklistLen returns how many task-list items the selected ticket has.
func (l *TicketList) ChecklistLen() int {
t, ok := l.Selected()
if !ok {
return 0
}
return len(ticket.ListCheckboxes(t.Description))
}
// MoveChecklistDown advances the checkbox cursor (wraps).
func (l *TicketList) MoveChecklistDown() {
n := l.ChecklistLen()
if n == 0 {
return
}
l.ChecklistCursor = (l.ChecklistCursor + 1) % n
}
// MoveChecklistUp moves the checkbox cursor up (wraps).
func (l *TicketList) MoveChecklistUp() {
n := l.ChecklistLen()
if n == 0 {
return
}
l.ChecklistCursor = (l.ChecklistCursor - 1 + n) % n
}
// SetChecklistCursor sets the checkbox highlight index (clamped).
func (l *TicketList) SetChecklistCursor(idx int) {
l.ChecklistCursor = idx
l.clampChecklistCursor()
}
func (l *TicketList) clampChecklistCursor() {
n := l.ChecklistLen()
if n == 0 {
l.ChecklistCursor = 0
return
}
if l.ChecklistCursor < 0 {
l.ChecklistCursor = 0
}
if l.ChecklistCursor >= n {
l.ChecklistCursor = n - 1
}
}
// View renders only the visible ticket cards.
func (l *TicketList) View() string {
if l.Height < 1 {
@@ -126,16 +181,15 @@ func (l *TicketList) renderCard(t ticket.Ticket, selected bool) string {
var content string
if selected {
// Expanded: headline + optional spent + glamour description.
// Expanded: headline + optional spent + description (interactive checkboxes).
content = headerLine
if spent, ok := l.SpentByID[t.ID]; ok && spent > 0 {
content += "\n" + lipgloss.NewStyle().Faint(true).Render(
"spent: "+ticket.FormatDurationHuman(spent),
)
}
desc := strings.TrimSpace(t.Description)
if desc != "" {
content += "\n" + RenderMarkdown(desc, innerWidth)
if strings.TrimSpace(t.Description) != "" {
content += "\n" + RenderDescription(t.Description, innerWidth, l.ChecklistCursor)
}
} else {
content = headerLine