feat(teams): restore active tasks on session switch via teams.tasks.active-by-session RPC

This commit is contained in:
viettranx committed 2026-03-25 18:26:31 +07:00
1 parent 1355e4e0cb
commit f793bba0e0
7 files changed
+105 -3

No files matched your search

+1
View File
@@ -47,6 +47,7 @@ func (m *TeamsMethods) Register(router *gateway.MethodRouter) {
router.Register(protocol.MethodTeamsGet, m.handleGet)
router.Register(protocol.MethodTeamsDelete, m.handleDelete)
router.Register(protocol.MethodTeamsTaskList, m.handleTaskList)
router.Register(protocol.MethodTeamsTaskActiveBySession, m.handleTaskActiveBySession)
router.Register(protocol.MethodTeamsTaskApprove, m.handleTaskApprove)
router.Register(protocol.MethodTeamsTaskReject, m.handleTaskReject)
router.Register(protocol.MethodTeamsMembersAdd, m.handleAddMember)
+62
View File
@@ -195,6 +195,68 @@ func (m *TeamsMethods) handleTaskList(ctx context.Context, client *gateway.Clien
}))
}
// --- Active Tasks by Session ---
func (m *TeamsMethods) handleTaskActiveBySession(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
locale := store.LocaleFromContext(ctx)
if m.teamStore == nil {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, i18n.T(locale, i18n.MsgTeamsNotConfigured)))
return
}
var params struct {
SessionKey string `json:"sessionKey"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgInvalidJSON)))
return
}
if params.SessionKey == "" {
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"tasks": []any{}}))
return
}
tasks, err := m.teamStore.ListActiveTasksByChatID(ctx, params.SessionKey)
if err != nil {
slog.Warn("teams.tasks.active-by-session failed", "session_key", params.SessionKey, "error", err)
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, err.Error()))
return
}
// Map to lightweight ActiveTeamTask shape for the sidebar
type activeTask struct {
TaskID string `json:"taskId"`
TaskNumber int `json:"taskNumber"`
Subject string `json:"subject"`
Status string `json:"status"`
OwnerAgentKey string `json:"ownerAgentKey,omitempty"`
OwnerDisplayName string `json:"ownerDisplayName,omitempty"`
ProgressPercent int `json:"progressPercent,omitempty"`
ProgressStep string `json:"progressStep,omitempty"`
CommentCount int `json:"commentCount,omitempty"`
AttachmentCount int `json:"attachmentCount,omitempty"`
}
result := make([]activeTask, 0, len(tasks))
for _, t := range tasks {
result = append(result, activeTask{
TaskID: t.ID.String(),
TaskNumber: t.TaskNumber,
Subject: t.Subject,
Status: t.Status,
OwnerAgentKey: t.OwnerAgentKey,
ProgressPercent: t.ProgressPercent,
ProgressStep: t.ProgressStep,
CommentCount: t.CommentCount,
AttachmentCount: t.AttachmentCount,
})
}
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{
"tasks": result,
}))
}
// --- Update (settings) ---
type teamsUpdateParams struct {
+30
View File
@@ -456,6 +456,36 @@ func (s *PGTeamStore) DeleteTasks(ctx context.Context, taskIDs []uuid.UUID, team
return deleted, rows.Err()
}
// ListActiveTasksByChatID returns non-terminal tasks for a given chat_id (session key).
// Used to restore the active tasks sidebar when switching back to a session.
func (s *PGTeamStore) ListActiveTasksByChatID(ctx context.Context, chatID string) ([]store.TeamTaskData, error) {
if chatID == "" {
return nil, nil
}
args := []any{chatID}
tenantWhere := ""
if !store.IsCrossTenant(ctx) {
tid := store.TenantIDFromContext(ctx)
if tid == uuid.Nil {
return nil, fmt.Errorf("tenant_id required")
}
tenantWhere = fmt.Sprintf(" AND t.tenant_id = $%d", len(args)+1)
args = append(args, tid)
}
rows, err := s.db.QueryContext(ctx,
`SELECT `+taskSelectCols+`
`+taskJoinClause+`
WHERE COALESCE(t.chat_id,'') = $1
AND t.status IN ('pending','in_progress','blocked','in_review')`+tenantWhere+`
ORDER BY t.task_number ASC
LIMIT 50`, args...)
if err != nil {
return nil, err
}
defer rows.Close()
return scanTaskRowsJoined(rows)
}
func scanTaskRowsJoined(rows *sql.Rows) ([]store.TeamTaskData, error) {
var tasks []store.TeamTaskData
for rows.Next() {
+1
View File
@@ -231,6 +231,7 @@ type TaskStore interface {
UpdateTaskProgress(ctx context.Context, taskID, teamID uuid.UUID, percent int, step string) error
RenewTaskLock(ctx context.Context, taskID, teamID uuid.UUID) error
ResetTaskStatus(ctx context.Context, taskID, teamID uuid.UUID) error
ListActiveTasksByChatID(ctx context.Context, chatID string) ([]TeamTaskData, error)
}
// TaskCommentStore manages task comments, audit events, and attachments.
+2 -1
View File
@@ -136,7 +136,8 @@ const (
MethodTeamsTaskCreate = "teams.tasks.create"
MethodTeamsTaskDelete = "teams.tasks.delete"
MethodTeamsTaskDeleteBulk = "teams.tasks.delete-bulk"
MethodTeamsTaskAssign = "teams.tasks.assign"
MethodTeamsTaskAssign = "teams.tasks.assign"
MethodTeamsTaskActiveBySession = "teams.tasks.active-by-session"
MethodTeamsMembersAdd = "teams.members.add"
MethodTeamsMembersRemove = "teams.members.remove"
MethodTeamsUpdate = "teams.update"
+1
View File
@@ -143,6 +143,7 @@ export const Methods = {
TEAMS_TASK_DELETE: "teams.tasks.delete",
TEAMS_TASK_DELETE_BULK: "teams.tasks.delete-bulk",
TEAMS_TASK_ASSIGN: "teams.tasks.assign",
TEAMS_TASK_ACTIVE_BY_SESSION: "teams.tasks.active-by-session",
TEAMS_MEMBERS_ADD: "teams.members.add",
TEAMS_MEMBERS_REMOVE: "teams.members.remove",
TEAMS_UPDATE: "teams.update",
@@ -139,12 +139,18 @@ export function useChatMessages(sessionKey: string, agentId: string) {
}
}, [ws, agentId, sessionKey]);
// Load history when session changes
// Load history and restore active tasks when session changes
useEffect(() => {
if (sessionKey) {
loadHistory();
// Restore active team tasks (fire-and-forget — teams module may not be configured)
ws.call<{ tasks?: ActiveTeamTask[] }>(Methods.TEAMS_TASK_ACTIVE_BY_SESSION, { sessionKey })
.then((res) => {
if (res.tasks && res.tasks.length > 0) setTeamTasks(res.tasks);
})
.catch(() => {}); // teams not configured or RPC not registered
}
}, [sessionKey, loadHistory]);
}, [sessionKey, loadHistory, ws]);
// Called before sending a message so the event handler knows to capture run.started
const expectRun = useCallback(() => {