Changeset 0.17.1 (#76)

This commit is contained in:
2026-02-28 01:40:31 +00:00
parent c9141a6896
commit 856dc9b0ac
64 changed files with 8037 additions and 1657 deletions

View File

@@ -89,7 +89,7 @@ func (h *MessageHandler) ListMessages(c *gin.Context) {
var total int
err := database.DB.QueryRow(
`SELECT COUNT(*) FROM messages WHERE channel_id = $1 AND deleted_at IS NULL`,
database.Q(`SELECT COUNT(*) FROM messages WHERE channel_id = $1 AND deleted_at IS NULL`),
channelID,
).Scan(&total)
if err != nil {
@@ -97,14 +97,14 @@ func (h *MessageHandler) ListMessages(c *gin.Context) {
return
}
rows, err := database.DB.Query(`
rows, err := database.DB.Query(database.Q(`
SELECT id, channel_id, role, content, model, tokens_used, parent_id,
sibling_index, created_at
FROM messages
WHERE channel_id = $1 AND deleted_at IS NULL
ORDER BY created_at ASC
LIMIT $2 OFFSET $3
`, channelID, perPage, offset)
`), channelID, perPage, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list messages"})
return
@@ -123,9 +123,14 @@ func (h *MessageHandler) ListMessages(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to scan message"})
return
}
msg.SiblingCount = getSiblingCount(channelID, msg.ParentID)
messages = append(messages, msg)
}
rows.Close() // release connection before sibling queries
// Enrich with sibling counts (requires DB access, must happen after rows are closed)
for i := range messages {
messages[i].SiblingCount = getSiblingCount(channelID, messages[i].ParentID)
}
c.JSON(http.StatusOK, paginatedResponse{
Data: messages,
@@ -191,27 +196,53 @@ func (h *MessageHandler) CreateMessage(c *gin.Context) {
}
var msg messageResponse
err := database.DB.QueryRow(`
INSERT INTO messages (channel_id, role, content, model, parent_id,
participant_type, participant_id, sibling_index)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, channel_id, role, content, model, tokens_used, parent_id,
sibling_index, created_at
`, channelID, req.Role, req.Content, req.Model, parentID,
participantType, participantID, siblingIdx,
).Scan(
&msg.ID, &msg.ChannelID, &msg.Role, &msg.Content,
&msg.Model, &msg.TokensUsed, &msg.ParentID,
&msg.SiblingIndex, &msg.CreatedAt,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create message"})
return
if database.IsSQLite() {
newID := store.NewID()
_, err := database.DB.Exec(`
INSERT INTO messages (id, channel_id, role, content, model, parent_id,
participant_type, participant_id, sibling_index)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, newID, channelID, req.Role, req.Content, req.Model, parentID,
participantType, participantID, siblingIdx)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create message"})
return
}
err = database.DB.QueryRow(`
SELECT id, channel_id, role, content, model, tokens_used, parent_id,
sibling_index, created_at
FROM messages WHERE id = ?`, newID).Scan(
&msg.ID, &msg.ChannelID, &msg.Role, &msg.Content,
&msg.Model, &msg.TokensUsed, &msg.ParentID,
&msg.SiblingIndex, &msg.CreatedAt,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read created message"})
return
}
} else {
err := database.DB.QueryRow(`
INSERT INTO messages (channel_id, role, content, model, parent_id,
participant_type, participant_id, sibling_index)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, channel_id, role, content, model, tokens_used, parent_id,
sibling_index, created_at
`, channelID, req.Role, req.Content, req.Model, parentID,
participantType, participantID, siblingIdx,
).Scan(
&msg.ID, &msg.ChannelID, &msg.Role, &msg.Content,
&msg.Model, &msg.TokensUsed, &msg.ParentID,
&msg.SiblingIndex, &msg.CreatedAt,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create message"})
return
}
}
msg.SiblingCount = getSiblingCount(channelID, msg.ParentID)
_ = updateCursor(channelID, userID, msg.ID)
_, _ = database.DB.Exec(`UPDATE channels SET updated_at = NOW() WHERE id = $1`, channelID)
_, _ = database.DB.Exec(database.Q(`UPDATE channels SET updated_at = NOW() WHERE id = $1`), channelID)
c.JSON(http.StatusCreated, msg)
}
@@ -240,10 +271,10 @@ func (h *MessageHandler) EditMessage(c *gin.Context) {
// Load target — must exist, belong to channel, be a user message
var targetParentID *string
var targetRole string
err := database.DB.QueryRow(`
err := database.DB.QueryRow(database.Q(`
SELECT parent_id, role FROM messages
WHERE id = $1 AND channel_id = $2 AND deleted_at IS NULL
`, messageID, channelID).Scan(&targetParentID, &targetRole)
`), messageID, channelID).Scan(&targetParentID, &targetRole)
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "message not found"})
@@ -262,18 +293,39 @@ func (h *MessageHandler) EditMessage(c *gin.Context) {
siblingIdx := nextSiblingIndex(channelID, targetParentID)
var msg messageResponse
err = database.DB.QueryRow(`
INSERT INTO messages (channel_id, role, content, parent_id,
participant_type, participant_id, sibling_index)
VALUES ($1, 'user', $2, $3, 'user', $4, $5)
RETURNING id, channel_id, role, content, model, tokens_used, parent_id,
sibling_index, created_at
`, channelID, req.Content, targetParentID, userID, siblingIdx,
).Scan(
&msg.ID, &msg.ChannelID, &msg.Role, &msg.Content,
&msg.Model, &msg.TokensUsed, &msg.ParentID,
&msg.SiblingIndex, &msg.CreatedAt,
)
if database.IsSQLite() {
newID := store.NewID()
_, err = database.DB.Exec(`
INSERT INTO messages (id, channel_id, role, content, parent_id,
participant_type, participant_id, sibling_index)
VALUES (?, ?, 'user', ?, ?, 'user', ?, ?)
`, newID, channelID, req.Content, targetParentID, userID, siblingIdx)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create edit"})
return
}
err = database.DB.QueryRow(`
SELECT id, channel_id, role, content, model, tokens_used, parent_id,
sibling_index, created_at
FROM messages WHERE id = ?`, newID).Scan(
&msg.ID, &msg.ChannelID, &msg.Role, &msg.Content,
&msg.Model, &msg.TokensUsed, &msg.ParentID,
&msg.SiblingIndex, &msg.CreatedAt,
)
} else {
err = database.DB.QueryRow(`
INSERT INTO messages (channel_id, role, content, parent_id,
participant_type, participant_id, sibling_index)
VALUES ($1, 'user', $2, $3, 'user', $4, $5)
RETURNING id, channel_id, role, content, model, tokens_used, parent_id,
sibling_index, created_at
`, channelID, req.Content, targetParentID, userID, siblingIdx,
).Scan(
&msg.ID, &msg.ChannelID, &msg.Role, &msg.Content,
&msg.Model, &msg.TokensUsed, &msg.ParentID,
&msg.SiblingIndex, &msg.CreatedAt,
)
}
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create edit"})
return
@@ -282,7 +334,7 @@ func (h *MessageHandler) EditMessage(c *gin.Context) {
// Cursor now points to the new sibling (it's a leaf — no children yet)
_ = updateCursor(channelID, userID, msg.ID)
_, _ = database.DB.Exec(`UPDATE channels SET updated_at = NOW() WHERE id = $1`, channelID)
_, _ = database.DB.Exec(database.Q(`UPDATE channels SET updated_at = NOW() WHERE id = $1`), channelID)
c.JSON(http.StatusCreated, msg)
}
@@ -315,10 +367,10 @@ func (h *MessageHandler) Regenerate(c *gin.Context) {
// Load target message
var targetParentID *string
var targetRole string
err := database.DB.QueryRow(`
err := database.DB.QueryRow(database.Q(`
SELECT parent_id, role FROM messages
WHERE id = $1 AND channel_id = $2 AND deleted_at IS NULL
`, messageID, channelID).Scan(&targetParentID, &targetRole)
`), messageID, channelID).Scan(&targetParentID, &targetRole)
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "message not found"})
@@ -399,7 +451,7 @@ func (h *MessageHandler) Regenerate(c *gin.Context) {
// Fallback: channel's stored model
if model == "" {
var channelModel *string
_ = database.DB.QueryRow(`SELECT model FROM channels WHERE id = $1`, channelID).Scan(&channelModel)
_ = database.DB.QueryRow(database.Q(`SELECT model FROM channels WHERE id = $1`), channelID).Scan(&channelModel)
if channelModel != nil {
model = *channelModel
}
@@ -427,7 +479,7 @@ func (h *MessageHandler) Regenerate(c *gin.Context) {
llmMessages = append(llmMessages, providers.Message{Role: "system", Content: presetSystemPrompt})
} else {
var systemPrompt *string
_ = database.DB.QueryRow(`SELECT system_prompt FROM channels WHERE id = $1`, channelID).Scan(&systemPrompt)
_ = database.DB.QueryRow(database.Q(`SELECT system_prompt FROM channels WHERE id = $1`), channelID).Scan(&systemPrompt)
if systemPrompt != nil && *systemPrompt != "" {
llmMessages = append(llmMessages, providers.Message{Role: "system", Content: *systemPrompt})
}
@@ -480,16 +532,31 @@ func (h *MessageHandler) Regenerate(c *gin.Context) {
}
var newID string
err := database.DB.QueryRow(`
INSERT INTO messages (channel_id, role, content, model, tool_calls,
parent_id, participant_type, participant_id, sibling_index)
VALUES ($1, 'assistant', $2, $3, $4, $5, 'model', $6, $7)
RETURNING id
`, channelID, result.Content, model, tcVal, newParentID, model, siblingIdx).Scan(&newID)
if err != nil {
log.Printf("Failed to persist regenerated message: %v", err)
if database.IsSQLite() {
newID = store.NewID()
_, err := database.DB.Exec(`
INSERT INTO messages (id, channel_id, role, content, model, tool_calls,
parent_id, participant_type, participant_id, sibling_index)
VALUES (?, ?, 'assistant', ?, ?, ?, ?, 'model', ?, ?)
`, newID, channelID, result.Content, model, tcVal, newParentID, model, siblingIdx)
if err != nil {
log.Printf("Failed to persist regenerated message: %v", err)
newID = ""
}
} else {
err := database.DB.QueryRow(`
INSERT INTO messages (channel_id, role, content, model, tool_calls,
parent_id, participant_type, participant_id, sibling_index)
VALUES ($1, 'assistant', $2, $3, $4, $5, 'model', $6, $7)
RETURNING id
`, channelID, result.Content, model, tcVal, newParentID, model, siblingIdx).Scan(&newID)
if err != nil {
log.Printf("Failed to persist regenerated message: %v", err)
newID = ""
}
}
if newID != "" {
_ = updateCursor(channelID, userID, newID)
flusher, _ := c.Writer.(http.Flusher)
@@ -505,7 +572,7 @@ func (h *MessageHandler) Regenerate(c *gin.Context) {
}
}
_, _ = database.DB.Exec(`UPDATE channels SET updated_at = NOW() WHERE id = $1`, channelID)
_, _ = database.DB.Exec(database.Q(`UPDATE channels SET updated_at = NOW() WHERE id = $1`), channelID)
}
// Log usage for regeneration
@@ -536,10 +603,10 @@ func (h *MessageHandler) UpdateCursor(c *gin.Context) {
// Verify the target message belongs to this channel
var msgChannelID string
err := database.DB.QueryRow(`
err := database.DB.QueryRow(database.Q(`
SELECT channel_id FROM messages
WHERE id = $1 AND deleted_at IS NULL
`, req.ActiveLeafID).Scan(&msgChannelID)
`), req.ActiveLeafID).Scan(&msgChannelID)
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "message not found"})
@@ -609,7 +676,7 @@ func (h *MessageHandler) ListSiblings(c *gin.Context) {
func userOwnsChannel(c *gin.Context, channelID, userID string) bool {
var ownerID string
err := database.DB.QueryRow(
`SELECT user_id FROM channels WHERE id = $1`, channelID,
database.Q(`SELECT user_id FROM channels WHERE id = $1`), channelID,
).Scan(&ownerID)
if err == sql.ErrNoRows {