fix: don't collapse a genuine clarification/permission wait to REVIEW (#3549)
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
This commit is contained in:
@@ -103,15 +103,21 @@ export function KanbanTaskShell({
|
||||
// background-running task reads distinctly and never as done,
|
||||
// and carry the sidebar's rich "needs me" reading — pending clarification /
|
||||
// permission — so the header distinguishes waiting-for-input.
|
||||
function simpleTaskPendingFallback(task: Task | null) {
|
||||
if (!task) return {};
|
||||
return {
|
||||
taskId: task.id,
|
||||
taskPendingAction: task.task_pending_action,
|
||||
statusSummary: task.status_summary,
|
||||
primarySessionState: task.primary_session_state,
|
||||
primarySessionPendingAction: task.primary_session_pending_action,
|
||||
};
|
||||
}
|
||||
|
||||
function simpleTaskHeaderData(task: Task | null) {
|
||||
return {
|
||||
primarySessionId: task?.primary_session_id,
|
||||
pendingFallback: {
|
||||
taskId: task?.id,
|
||||
taskPendingAction: task?.task_pending_action,
|
||||
primarySessionState: task?.primary_session_state,
|
||||
primarySessionPendingAction: task?.primary_session_pending_action,
|
||||
},
|
||||
pendingFallback: simpleTaskPendingFallback(task),
|
||||
identifier: task?.id?.slice(0, 8),
|
||||
title: task?.title ?? t("tasks:loading"),
|
||||
state: task?.state ?? null,
|
||||
|
||||
@@ -171,6 +171,7 @@ export function TaskListRowPrimaryContent({
|
||||
const pendingInput = useTaskPendingInput(task.primary_session_id, {
|
||||
taskId: task.id,
|
||||
taskPendingAction: task.task_pending_action,
|
||||
statusSummary: task.status_summary,
|
||||
primarySessionState: task.primary_session_state,
|
||||
primarySessionPendingAction: task.primary_session_pending_action,
|
||||
});
|
||||
|
||||
@@ -368,6 +368,7 @@ function KanbanCardActions({
|
||||
const pendingInput = useTaskPendingInput(task.primarySessionId, {
|
||||
taskId: task.id,
|
||||
taskPendingAction: task.taskPendingAction,
|
||||
statusSummary: task.statusSummary,
|
||||
primarySessionState: task.primarySessionState,
|
||||
primarySessionPendingAction: task.primarySessionPendingAction,
|
||||
});
|
||||
|
||||
@@ -126,6 +126,7 @@ export function Graph2StepNode({
|
||||
const pendingInput = useTaskPendingInput(task.primarySessionId, {
|
||||
taskId: task.id,
|
||||
taskPendingAction: task.taskPendingAction,
|
||||
statusSummary: task.statusSummary,
|
||||
primarySessionState: task.primarySessionState,
|
||||
primarySessionPendingAction: task.primarySessionPendingAction,
|
||||
});
|
||||
|
||||
@@ -80,6 +80,7 @@ function DraggableTaskChip({
|
||||
const pendingInput = useTaskPendingInput(task.primarySessionId, {
|
||||
taskId: task.id,
|
||||
taskPendingAction: task.taskPendingAction,
|
||||
statusSummary: task.statusSummary,
|
||||
primarySessionState: task.primarySessionState,
|
||||
primarySessionPendingAction: task.primarySessionPendingAction,
|
||||
});
|
||||
@@ -117,6 +118,7 @@ function TaskChipPreview({ task }: { task: Task }) {
|
||||
const pendingInput = useTaskPendingInput(task.primarySessionId, {
|
||||
taskId: task.id,
|
||||
taskPendingAction: task.taskPendingAction,
|
||||
statusSummary: task.statusSummary,
|
||||
primarySessionState: task.primarySessionState,
|
||||
primarySessionPendingAction: task.primarySessionPendingAction,
|
||||
});
|
||||
|
||||
@@ -193,6 +193,22 @@ describe("useTaskPendingInput", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("useTaskPendingInput task status summary authority", () => {
|
||||
it("prefers the task status summary over a stale legacy task snapshot", () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useTaskPendingInput("session-1", {
|
||||
taskId: "task-1",
|
||||
taskPendingAction: "clarification",
|
||||
statusSummary: { pending_action: "permission" },
|
||||
} as Parameters<typeof useTaskPendingInput>[1]),
|
||||
{ wrapper: wrapper() },
|
||||
);
|
||||
|
||||
expect(result.current).toEqual({ clarification: false, permission: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("useTaskPendingInput pending clarification turn authority", () => {
|
||||
it("preserves pending clarification while session state authority is still loading", () => {
|
||||
const olderTurn = turn("turn-older", PRIMARY_SESSION_ID, BASE_TIMESTAMP);
|
||||
|
||||
@@ -15,6 +15,7 @@ const NONE: PendingInput = { clarification: false, permission: false };
|
||||
export type PendingInputFallback = {
|
||||
taskId?: string | null;
|
||||
taskPendingAction?: TaskPendingAction | null;
|
||||
statusSummary?: { pending_action?: TaskPendingAction | null } | null;
|
||||
primarySessionState?: string | null;
|
||||
primarySessionPendingAction?: TaskPendingAction | null;
|
||||
};
|
||||
@@ -39,6 +40,14 @@ function actionFlags(action: TaskPendingAction | null | undefined): PendingInput
|
||||
return { clarification: action === "clarification", permission: action === "permission" };
|
||||
}
|
||||
|
||||
function fallbackTaskPendingAction(
|
||||
fallback: PendingInputFallback | undefined,
|
||||
): TaskPendingAction | null | undefined {
|
||||
return fallback?.statusSummary != null
|
||||
? fallback.statusSummary.pending_action
|
||||
: fallback?.taskPendingAction;
|
||||
}
|
||||
|
||||
function loadedSessionFlags(
|
||||
messagesBySession: Record<string, Message[] | undefined>,
|
||||
turnsBySession: Record<string, readonly Turn[] | undefined>,
|
||||
@@ -122,7 +131,7 @@ function selectFlagsFromLoadedTaskSessions(
|
||||
session.pending_action,
|
||||
);
|
||||
},
|
||||
fallback?.taskPendingAction,
|
||||
fallbackTaskPendingAction(fallback),
|
||||
);
|
||||
if (!result.hasUnloadedMessages) {
|
||||
return { clarification: result.clarification, permission: result.permission };
|
||||
@@ -139,7 +148,7 @@ function selectFlagsFromPrimarySession(
|
||||
primarySession: PrimarySessionProjection,
|
||||
fallback: PendingInputFallback | undefined,
|
||||
): PendingInput {
|
||||
const taskSnapshot = actionFlags(fallback?.taskPendingAction);
|
||||
const taskSnapshot = actionFlags(fallbackTaskPendingAction(fallback));
|
||||
if (taskSnapshot.clarification || taskSnapshot.permission) return taskSnapshot;
|
||||
if (!primarySession.id) return NONE;
|
||||
if (messagesBySession[primarySession.id] !== undefined) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { needsAction } from "./needs-action";
|
||||
|
||||
describe("needsAction", () => {
|
||||
it("uses the task status summary when legacy pending data is stale", () => {
|
||||
expect(
|
||||
needsAction({
|
||||
state: "REVIEW",
|
||||
taskPendingAction: "clarification",
|
||||
statusSummary: { pending_action: "permission" },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("recognizes a pending action when the task remains in REVIEW", () => {
|
||||
expect(
|
||||
needsAction({
|
||||
state: "REVIEW",
|
||||
statusSummary: { pending_action: "clarification" },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not revive a legacy action after the status summary clears it", () => {
|
||||
expect(
|
||||
needsAction({
|
||||
state: "REVIEW",
|
||||
taskPendingAction: "clarification",
|
||||
statusSummary: {},
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TaskState } from "@/lib/types/http";
|
||||
import type { TaskPendingAction, TaskState } from "@/lib/types/http";
|
||||
|
||||
/**
|
||||
* Determines if a task needs user attention/action.
|
||||
@@ -6,11 +6,16 @@ import type { TaskState } from "@/lib/types/http";
|
||||
*/
|
||||
export function needsAction(task: {
|
||||
state?: TaskState;
|
||||
taskPendingAction?: TaskPendingAction | null;
|
||||
statusSummary?: { pending_action?: TaskPendingAction | null } | null;
|
||||
reviewStatus?: "pending" | "approved" | "changes_requested" | "rejected" | null;
|
||||
}): boolean {
|
||||
const pendingAction =
|
||||
task.statusSummary != null ? task.statusSummary.pending_action : task.taskPendingAction;
|
||||
return (
|
||||
(task.reviewStatus === "pending" && task.state !== "IN_PROGRESS") ||
|
||||
task.reviewStatus === "changes_requested" ||
|
||||
pendingAction != null ||
|
||||
task.state === "WAITING_FOR_INPUT"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user