diff --git a/internal/gateway/methods/teams.go b/internal/gateway/methods/teams.go index a00c865b..c8d91ca1 100644 --- a/internal/gateway/methods/teams.go +++ b/internal/gateway/methods/teams.go @@ -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) diff --git a/internal/gateway/methods/teams_crud.go b/internal/gateway/methods/teams_crud.go index 58bcf229..392a882a 100644 --- a/internal/gateway/methods/teams_crud.go +++ b/internal/gateway/methods/teams_crud.go @@ -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, ¶ms); 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 { diff --git a/internal/store/pg/teams_tasks.go b/internal/store/pg/teams_tasks.go index d879ce0c..bcd2ba0d 100644 --- a/internal/store/pg/teams_tasks.go +++ b/internal/store/pg/teams_tasks.go @@ -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() { diff --git a/internal/store/team_store.go b/internal/store/team_store.go index f0189766..0ce69809 100644 --- a/internal/store/team_store.go +++ b/internal/store/team_store.go @@ -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. diff --git a/pkg/protocol/methods.go b/pkg/protocol/methods.go index cd1eecbe..6ddb7388 100644 --- a/pkg/protocol/methods.go +++ b/pkg/protocol/methods.go @@ -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" diff --git a/ui/web/src/api/protocol.ts b/ui/web/src/api/protocol.ts index ae600172..34bb536b 100644 --- a/ui/web/src/api/protocol.ts +++ b/ui/web/src/api/protocol.ts @@ -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", diff --git a/ui/web/src/pages/chat/hooks/use-chat-messages.ts b/ui/web/src/pages/chat/hooks/use-chat-messages.ts index 26ff3157..bf502c99 100644 --- a/ui/web/src/pages/chat/hooks/use-chat-messages.ts +++ b/ui/web/src/pages/chat/hooks/use-chat-messages.ts @@ -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(() => {