Files
kandev/apps/web/lib/commands/task-result-activity.ts
T
2026-08-31 21:07:18 +00:00

96 lines
3.6 KiB
TypeScript

import type { KanbanState } from "@/lib/state/slices/kanban/types";
import { pickFreshestStatusSummary } from "@/lib/task-status-summary";
import type { ForegroundActivity, Task, TaskSessionState, TaskState } from "@/lib/types/http";
export type CommandPanelLiveTask = KanbanState["tasks"][number];
export type TaskResultActivity = {
state: TaskState;
workflowStepId: string;
sessionState?: TaskSessionState;
foregroundActivity?: ForegroundActivity | null;
hasPendingClarification: boolean;
hasPendingPermission: boolean;
isOnLastWorkflowStep: boolean;
interrupted?: boolean;
};
function currentLiveTask(task: Task, liveTask?: CommandPanelLiveTask) {
if (!liveTask) return undefined;
if (!liveTask.updatedAt) {
// Legacy boot payloads can omit updatedAt. Treat this projection as the
// current WebSocket-backed reading so its lifecycle and cleared activity
// fields do not fall back to an older search response.
return liveTask;
}
const liveUpdatedAt = Date.parse(liveTask.updatedAt);
const resultUpdatedAt = Date.parse(task.updated_at);
if (!Number.isFinite(liveUpdatedAt) || !Number.isFinite(resultUpdatedAt)) return liveTask;
return liveUpdatedAt >= resultUpdatedAt ? liveTask : undefined;
}
function resolvePendingAction(
task: Task,
liveTask?: CommandPanelLiveTask,
directLiveTask?: CommandPanelLiveTask,
) {
const summary = pickFreshestStatusSummary(liveTask?.statusSummary, task.status_summary);
return (
summary?.pending_action ??
directLiveTask?.taskPendingAction ??
directLiveTask?.primarySessionPendingAction ??
task.task_pending_action ??
task.primary_session_pending_action
);
}
function resolveSessionState(
task: Task,
liveTask?: CommandPanelLiveTask,
directLiveTask?: CommandPanelLiveTask,
) {
const summary = pickFreshestStatusSummary(liveTask?.statusSummary, task.status_summary);
return (
summary?.primary_session?.state ??
(directLiveTask?.primarySessionState as TaskSessionState | null | undefined) ??
task.primary_session_state ??
undefined
);
}
function resolveForegroundActivity(
task: Task,
liveTask?: CommandPanelLiveTask,
directLiveTask?: CommandPanelLiveTask,
) {
const liveSummary = liveTask?.statusSummary;
const summary = pickFreshestStatusSummary(liveSummary, task.status_summary);
if (summary === liveSummary && liveSummary) return liveSummary.foreground_activity ?? null;
if (directLiveTask) return directLiveTask.foregroundActivity ?? null;
return summary?.foreground_activity ?? task.foreground_activity ?? null;
}
/** Merge the search response with the live task projection already kept current by WebSocket events. */
export function resolveTaskResultActivity(
task: Task,
liveTask?: CommandPanelLiveTask,
lastStepIdByWorkflowId?: ReadonlyMap<string, string>,
): TaskResultActivity {
const directLiveTask = currentLiveTask(task, liveTask);
const pendingAction = resolvePendingAction(task, liveTask, directLiveTask);
const effectiveWorkflowId = directLiveTask?.workflowId ?? task.workflow_id;
const effectiveWorkflowStepId = directLiveTask?.workflowStepId ?? task.workflow_step_id;
return {
state: directLiveTask?.state ?? task.state,
workflowStepId: effectiveWorkflowStepId,
sessionState: resolveSessionState(task, liveTask, directLiveTask),
foregroundActivity: resolveForegroundActivity(task, liveTask, directLiveTask),
hasPendingClarification: pendingAction === "clarification",
hasPendingPermission: pendingAction === "permission",
isOnLastWorkflowStep:
lastStepIdByWorkflowId?.get(effectiveWorkflowId) === effectiveWorkflowStepId,
interrupted: directLiveTask?.interrupted ?? task.interrupted,
};
}