package treepath import ( "database/sql" "fmt" "git.gobha.me/xcaliber/chat-switchboard/database" ) // GetSiblingCount returns how many live siblings share the same parent_id. // Root messages (parent_id IS NULL) count all roots in the same channel. func GetSiblingCount(channelID string, parentID *string) int { var count int if parentID == nil { database.DB.QueryRow(database.Q(` SELECT COUNT(*) FROM messages WHERE channel_id = $1 AND parent_id IS NULL AND deleted_at IS NULL `), channelID).Scan(&count) } else { database.DB.QueryRow(database.Q(` SELECT COUNT(*) FROM messages WHERE parent_id = $1 AND deleted_at IS NULL `), *parentID).Scan(&count) } if count == 0 { count = 1 } return count } // GetSiblings returns all live siblings of a given message (including itself), // ordered by sibling_index. func GetSiblings(messageID string) ([]SiblingInfo, int, error) { // First get the parent_id of the target message var parentID *string var channelID string err := database.DB.QueryRow(database.Q(` SELECT parent_id, channel_id FROM messages WHERE id = $1 AND deleted_at IS NULL `), messageID).Scan(&parentID, &channelID) if err != nil { return nil, 0, fmt.Errorf("message not found: %w", err) } var rows *sql.Rows if parentID == nil { // Root messages: siblings are other roots in the same channel rows, err = database.DB.Query(database.Q(` SELECT id, role, model, sibling_index, SUBSTR(content, 1, 80), created_at FROM messages WHERE channel_id = $1 AND parent_id IS NULL AND deleted_at IS NULL ORDER BY sibling_index, created_at `), channelID) } else { rows, err = database.DB.Query(database.Q(` SELECT id, role, model, sibling_index, SUBSTR(content, 1, 80), created_at FROM messages WHERE parent_id = $1 AND deleted_at IS NULL ORDER BY sibling_index, created_at `), *parentID) } if err != nil { return nil, 0, err } defer rows.Close() var siblings []SiblingInfo currentIdx := 0 for i := 0; rows.Next(); i++ { var s SiblingInfo if err := rows.Scan(&s.ID, &s.Role, &s.Model, &s.SiblingIndex, &s.Preview, &s.CreatedAt); err != nil { return nil, 0, err } if s.ID == messageID { currentIdx = i } siblings = append(siblings, s) } return siblings, currentIdx, nil } // FindLeafFromMessage walks down from a message to find the deepest descendant. // Follows the first child (lowest sibling_index) at each level. // Used when switching branches: clicking a mid-tree sibling navigates to its leaf. func FindLeafFromMessage(messageID string) (string, error) { var leafID string err := database.DB.QueryRow(database.Q(` WITH RECURSIVE descendants AS ( SELECT id, 0 AS depth FROM messages WHERE id = $1 AND deleted_at IS NULL UNION ALL SELECT child.id, d.depth + 1 FROM messages child JOIN descendants d ON child.parent_id = d.id WHERE child.deleted_at IS NULL AND child.sibling_index = ( SELECT MIN(sibling_index) FROM messages WHERE parent_id = d.id AND deleted_at IS NULL ) ) SELECT id FROM descendants ORDER BY depth DESC LIMIT 1 `), messageID).Scan(&leafID) if err != nil { return messageID, nil // if anything fails, just use the message itself } return leafID, nil }