Files
kandev/apps/backend/internal/task/service/quick_chat_sessions.go
T
João Salavisa 9be979827d fix: sync quick chats and their names across devices (#2085)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:01:22 +01:00

161 lines
4.6 KiB
Go

package service
import (
"context"
"sort"
"time"
"github.com/kandev/kandev/internal/task/models"
)
// Quick-chat tab kinds. A workspace config chat is a quick chat whose task
// carries the config_mode metadata flag; everything else is a regular chat.
const (
QuickChatKindChat = "chat"
QuickChatKindConfig = "config"
// quickChatDefaultTitle is the placeholder title used when a quick chat is
// started without a name. It is not surfaced as a tab label.
quickChatDefaultTitle = "Quick Chat"
quickChatListPageSize = 1000
)
// QuickChatSession is one restorable quick-chat tab: the ephemeral task's
// primary session plus the metadata the UI needs to render its tab.
type QuickChatSession struct {
SessionID string
TaskID string
WorkspaceID string
Kind string
Name string
AgentProfileID string
Session *models.TaskSession
createdAt time.Time
lastActivityAt time.Time
}
// ListQuickChatSessions returns the workspace's restorable quick-chat sessions,
// most recently active first. It is the single source of truth for the quick
// chat tab strip: the boot payload and the runtime resync endpoint both read it,
// so a client that reloads and a client that resyncs converge on the same list.
func (s *Service) ListQuickChatSessions(ctx context.Context, workspaceID string) ([]QuickChatSession, error) {
tasks, err := s.listQuickChatTasks(ctx, workspaceID)
if err != nil {
return nil, err
}
if len(tasks) == 0 {
return []QuickChatSession{}, nil
}
ids := make([]string, 0, len(tasks))
for _, task := range tasks {
if task != nil {
ids = append(ids, task.ID)
}
}
primaryByTask, err := s.GetPrimarySessionInfoForTasks(ctx, ids)
if err != nil {
return nil, err
}
items := make([]QuickChatSession, 0, len(tasks))
for _, task := range tasks {
if !IsRestorableQuickChatTask(task) {
continue
}
primary := primaryByTask[task.ID]
if primary == nil || primary.ID == "" {
continue
}
items = append(items, newQuickChatSession(task, primary))
}
sortQuickChatSessions(items)
return items, nil
}
func newQuickChatSession(task *models.Task, primary *models.TaskSession) QuickChatSession {
item := QuickChatSession{
SessionID: primary.ID,
TaskID: task.ID,
WorkspaceID: task.WorkspaceID,
Kind: quickChatSessionKind(task),
AgentProfileID: quickChatAgentProfileID(task, primary),
Session: primary,
createdAt: task.CreatedAt,
lastActivityAt: quickChatLastActivityAt(task, primary),
}
if task.Title != "" && task.Title != quickChatDefaultTitle {
item.Name = task.Title
}
return item
}
// sortQuickChatSessions orders tabs by most recent activity, falling back to
// creation order and then task ID so the sequence is stable across clients.
func sortQuickChatSessions(items []QuickChatSession) {
sort.SliceStable(items, func(i, j int) bool {
if !items[i].lastActivityAt.Equal(items[j].lastActivityAt) {
return items[i].lastActivityAt.After(items[j].lastActivityAt)
}
if !items[i].createdAt.Equal(items[j].createdAt) {
return items[i].createdAt.Before(items[j].createdAt)
}
return items[i].TaskID < items[j].TaskID
})
}
func (s *Service) listQuickChatTasks(ctx context.Context, workspaceID string) ([]*models.Task, error) {
var all []*models.Task
for page := 1; ; page++ {
tasks, total, err := s.ListTasksByWorkspace(
ctx, workspaceID, "", "", "", page, quickChatListPageSize, "", false, false, true, false,
)
if err != nil {
return nil, err
}
all = append(all, tasks...)
if len(tasks) == 0 || len(all) >= total {
return all, nil
}
}
}
// IsRestorableQuickChatTask reports whether a task backs a quick-chat tab.
// Workflow-bound and automation-run tasks are ephemeral for other reasons and
// must never surface in the tab strip.
func IsRestorableQuickChatTask(task *models.Task) bool {
return task != nil &&
task.IsEphemeral &&
task.WorkflowID == "" &&
task.Origin != models.TaskOriginAutomationRun
}
func quickChatLastActivityAt(task *models.Task, primary *models.TaskSession) time.Time {
if primary.UpdatedAt.After(task.UpdatedAt) {
return primary.UpdatedAt
}
return task.UpdatedAt
}
func quickChatSessionKind(task *models.Task) string {
if task != nil {
if configMode, ok := task.Metadata["config_mode"].(bool); ok && configMode {
return QuickChatKindConfig
}
}
return QuickChatKindChat
}
func quickChatAgentProfileID(task *models.Task, primary *models.TaskSession) string {
if task != nil {
if value, ok := task.Metadata[models.MetaKeyAgentProfileID].(string); ok {
return value
}
}
if primary != nil {
return primary.AgentProfileID
}
return ""
}