Skip to content
Merged
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
47 changes: 47 additions & 0 deletions cmd/tui_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ type planModel struct {
// Database watcher for cache invalidation
beadsWatcher *watcher.Watcher
beadsClient *beads.Client

// New bead animation tracking
newBeads map[string]time.Time // beadID -> creation timestamp for animation
}

// newPlanModel creates a new Plan Mode model
Expand Down Expand Up @@ -204,6 +207,7 @@ func newPlanModel(ctx context.Context, proj *project.Project) *planModel {
textInput: ti,
activeBeadSessions: make(map[string]bool),
selectedBeads: make(map[string]bool),
newBeads: make(map[string]time.Time),
createBeadPriority: 2,
zj: zellij.New(),
columnRatio: 0.4, // Default 40/60 split (issues/details)
Expand Down Expand Up @@ -417,6 +421,25 @@ func (m *planModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if msg.searchSeq < m.searchSeq {
return m, nil
}

var expireCmds []tea.Cmd
now := time.Now()

// Detect new beads by comparing with existing list
if len(m.beadItems) > 0 {
existingIDs := make(map[string]bool)
for _, bead := range m.beadItems {
existingIDs[bead.id] = true
}
for _, bead := range msg.beads {
// Mark as new if not in existing list and not already animated
if !existingIDs[bead.id] && m.newBeads[bead.id].IsZero() {
m.newBeads[bead.id] = now
expireCmds = append(expireCmds, scheduleNewBeadExpire(bead.id))
}
}
}

m.beadItems = msg.beads
if msg.activeSessions != nil {
m.activeBeadSessions = msg.activeSessions
Expand All @@ -427,7 +450,11 @@ func (m *planModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.statusMessage = msg.err.Error()
m.statusIsError = true
}

// Don't clear status message on success - let it persist until next action
if len(expireCmds) > 0 {
return m, tea.Batch(expireCmds...)
}
return m, nil

case planTickMsg:
Expand Down Expand Up @@ -556,6 +583,11 @@ func (m *planModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.statusIsError = false
return m, nil

case newBeadExpireMsg:
// Remove the bead from the newBeads map to stop animation
delete(m.newBeads, msg.beadID)
return m, nil

case tea.KeyMsg:
return m.handleKeyPress(msg)

Expand Down Expand Up @@ -664,6 +696,21 @@ func clearStatusAfter(d time.Duration) tea.Cmd {
})
}

// newBeadExpireMsg is sent when the animation for a new bead should expire
type newBeadExpireMsg struct {
beadID string
}

// newBeadAnimationDuration is how long newly created beads are highlighted
const newBeadAnimationDuration = 5 * time.Second

// scheduleNewBeadExpire returns a command that expires a new bead animation after the duration
func scheduleNewBeadExpire(beadID string) tea.Cmd {
return tea.Tick(newBeadAnimationDuration, func(t time.Time) tea.Msg {
return newBeadExpireMsg{beadID: beadID}
})
}

func (m *planModel) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
// Handle dialog-specific input
switch m.viewMode {
Expand Down
1 change: 1 addition & 0 deletions cmd/tui_plan_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func (m *planModel) createBead(title, beadType string, priority int, isEpic bool
items, err := m.loadBeads()
session := m.sessionName()
activeSessions, _ := m.proj.DB.GetBeadsWithActiveSessions(m.ctx, session)

return planDataMsg{beads: items, activeSessions: activeSessions, err: err}
}
}
Expand Down
31 changes: 30 additions & 1 deletion cmd/tui_plan_render.go
Original file line number Diff line number Diff line change
Expand Up @@ -897,10 +897,25 @@ func (m *planModel) renderBeadLine(i int, bead beadItem, panelWidth int) string
}

if i == m.beadsCursor {
// Use yellow background for newly created beads, regular blue for others
if _, isNew := m.newBeads[bead.id]; isNew {
newSelectedStyle := lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("0")). // Black text
Background(lipgloss.Color("226")) // Yellow background
return newSelectedStyle.Render(plainLine)
}
return tuiSelectedStyle.Render(plainLine)
}

// Hover style
// Hover style - also check for new beads
if _, isNew := m.newBeads[bead.id]; isNew {
newHoverStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("0")). // Black text
Background(lipgloss.Color("228")). // Lighter yellow
Bold(true)
return newHoverStyle.Render(plainLine)
}
hoverStyle := lipgloss.NewStyle().
Foreground(lipgloss.Color("255")).
Background(lipgloss.Color("240")).
Expand All @@ -913,6 +928,20 @@ func (m *planModel) renderBeadLine(i int, bead beadItem, panelWidth int) string
return tuiDimStyle.Render(line)
}

// Style new beads - apply yellow only to the title
if _, isNew := m.newBeads[bead.id]; isNew {
yellowTitle := tuiNewBeadStyle.Render(title)

var newLine string
if m.beadsExpanded {
newLine = fmt.Sprintf("%s%s%s%s %s [P%d %s] %s%s", selectionIndicator, treePrefix, workIndicator, icon, styledID, bead.priority, bead.beadType, sessionIndicator, yellowTitle)
} else {
newLine = fmt.Sprintf("%s%s%s%s %s %s%s %s", selectionIndicator, treePrefix, workIndicator, icon, styledID, styledType, sessionIndicator, yellowTitle)
}

return newLine
}

return line
}

Expand Down
5 changes: 5 additions & 0 deletions cmd/tui_shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ var (

typeDefaultStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("247")) // Gray for others

// New bead animation style
tuiNewBeadStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFF00")). // Bright yellow for newly created beads
Bold(true)
)

// Panel represents which panel position is currently focused (relative to current depth)
Expand Down