This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/treepath/siblings.go
2026-02-28 01:40:31 +00:00

115 lines
3.2 KiB
Go

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
}