FN-7582: clarify oversight nudge-disabled guideline copy
Reword the disabled-Nudge tooltip/helper text so it no longer reads as an overseer fault, and split it into two distinct reasons. - Add taskDetail.oversight.nudgeSuppressedTitle for the human-control-suppressed case (user-paused, done/archived, autoMerge:false human-review terminal), naming manual control as the cause - Reword taskDetail.oversight.nudgeDisabledTitle to a reassuring periodic-poll framing for the no-observation-yet case instead of implying the overseer is idle - Compute the shared nudgeDisabledReason once and reuse it at all four render sites (mobile menu + desktop inline title/helper) so the two copies can't drift - Add a changeset (patch) documenting the operator-facing copy fix - Extend TaskDetailModal.oversight-controls tests and test-helpers to cover the new suppressed-vs-disabled copy branching Files changed: .changeset/fn-7582-oversight-guideline-copy.md | 7 ++ .../dashboard/app/components/TaskDetailModal.tsx | 37 +++++++++-- .../TaskDetailModal.oversight-controls.test.tsx | 76 +++++++++++++++++++++- .../__tests__/TaskDetailModal.test-helpers.ts | 7 ++ 4 files changed, 121 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-7582 Fusion-Task-Lineage: bf2ceab4-2fb0-4686-a9e1-9e015502a521 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7582-oversight-guideline-copy.md
Normal file
7
.changeset/fn-7582-oversight-guideline-copy.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Clarify the oversight "Nudge unavailable" guideline so it no longer reads as an overseer fault.
|
||||
category: fix
|
||||
dev: TaskDetailModal oversight controls — reworded taskDetail.oversight.nudgeDisabledTitle and added taskDetail.oversight.nudgeSuppressedTitle to differentiate periodic-observation vs. manual-control states. No enablement/engine logic changed.
|
||||
@@ -3163,6 +3163,23 @@ export function TaskDetailContent({
|
||||
already uses — the server-side `evaluateOverseerHumanControl` guard is the
|
||||
real enforcement; this is a client-side disable heuristic only). Stop is
|
||||
hidden once oversight is already off — there is nothing left to stop.
|
||||
|
||||
FNXC:PlannerOversight 2026-07-05-00:00:
|
||||
FN-7582: the original disabled-Nudge copy ("overseer is not actively
|
||||
watching this task") read as a fault report — operators seeing it on a
|
||||
healthy IN PROGRESS task assumed the overseer had broken, when the real
|
||||
cause is benign: `pollPlannerOverseer` observes in-progress/in-review tasks
|
||||
on a bounded ~45s poll, and `plannerOverseerState` is only populated once
|
||||
that poll records a live observation for the current stage (FN-7531). The
|
||||
reworded copy below differentiates two distinct disabled reasons instead of
|
||||
one alarming message: (1) no observation yet — reassuring, periodic-poll
|
||||
framing (`nudgeDisabledTitle`); (2) human-control suppressed — user-paused,
|
||||
done/archived, or the `autoMerge:false` in-review human-review terminal —
|
||||
which gets its own distinct copy (`nudgeSuppressedTitle`) naming manual
|
||||
control as the reason instead of implying the overseer is idle. Neither
|
||||
`canNudgeOverseer` nor any other enablement/gating boolean changed; this is
|
||||
copy-only, selected via the already-computed `overseerHumanControlSuppressed`
|
||||
/ `overseerActive` booleans below.
|
||||
*/
|
||||
const overseerSnapshot = task.plannerOverseerState ?? null;
|
||||
const overseerActive = Boolean(overseerSnapshot);
|
||||
@@ -3173,6 +3190,18 @@ export function TaskDetailContent({
|
||||
const canNudgeOverseer = overseerActive && !oversightIsOff && !overseerHumanControlSuppressed;
|
||||
const canExplainOverseer = overseerActive && !oversightIsOff;
|
||||
const showStopOverseer = !oversightIsOff;
|
||||
/*
|
||||
FNXC:PlannerOversight 2026-07-05-00:00:
|
||||
FN-7582 shared disabled-reason string, computed once and reused at all four
|
||||
render sites (mobile menu title + helper, desktop inline title + helper) so
|
||||
the two copies can never drift out of sync. Picks the human-control-suppressed
|
||||
copy when suppression is the active cause even though `!overseerActive` may
|
||||
also be true in that state (e.g. a paused task that never got observed) —
|
||||
suppression is the more actionable/accurate explanation for the operator.
|
||||
*/
|
||||
const nudgeDisabledReason = overseerHumanControlSuppressed
|
||||
? t("taskDetail.oversight.nudgeSuppressedTitle", "Nudge is paused while this task is under manual control.")
|
||||
: t("taskDetail.oversight.nudgeDisabledTitle", "Nudge becomes available once the overseer is observing this task's current stage — it checks periodically.");
|
||||
const isActivityExpanded = activityExpanded && activeTab === "chat" && !isEditing;
|
||||
const isPlannerChatExpanded = plannerChatExpanded && activeTab === "planner-chat" && !isEditing;
|
||||
/*
|
||||
@@ -4006,7 +4035,7 @@ export function TaskDetailContent({
|
||||
}}
|
||||
onKeyDown={handleOversightMenuKeyDown}
|
||||
disabled={!canNudgeOverseer || isNudgingOverseer}
|
||||
title={canNudgeOverseer ? t("taskDetail.oversight.nudgeTitle", "Inject steering guidance into the current stage now") : t("taskDetail.oversight.nudgeDisabledTitle", "Nudge unavailable: overseer is not actively watching this task")}
|
||||
title={canNudgeOverseer ? t("taskDetail.oversight.nudgeTitle", "Inject steering guidance into the current stage now") : nudgeDisabledReason}
|
||||
aria-label={t("taskDetail.oversight.nudgeAriaLabel", "Manual nudge")}
|
||||
>
|
||||
{isNudgingOverseer ? <Loader2 className="spin" aria-hidden="true" /> : <Send aria-hidden="true" />}
|
||||
@@ -4015,7 +4044,7 @@ export function TaskDetailContent({
|
||||
)}
|
||||
{!oversightIsOff && !canNudgeOverseer && (
|
||||
<span className="detail-oversight-controls-helper" data-testid="detail-overseer-nudge-disabled-reason">
|
||||
{t("taskDetail.oversight.nudgeDisabledTitle", "Nudge unavailable: overseer is not actively watching this task")}
|
||||
{nudgeDisabledReason}
|
||||
</span>
|
||||
)}
|
||||
{showStopOverseer && (
|
||||
@@ -4126,7 +4155,7 @@ export function TaskDetailContent({
|
||||
void handleNudgeOverseer();
|
||||
}}
|
||||
disabled={!canNudgeOverseer || isNudgingOverseer}
|
||||
title={canNudgeOverseer ? t("taskDetail.oversight.nudgeTitle", "Inject steering guidance into the current stage now") : t("taskDetail.oversight.nudgeDisabledTitle", "Nudge unavailable: overseer is not actively watching this task")}
|
||||
title={canNudgeOverseer ? t("taskDetail.oversight.nudgeTitle", "Inject steering guidance into the current stage now") : nudgeDisabledReason}
|
||||
aria-label={t("taskDetail.oversight.nudgeAriaLabel", "Manual nudge")}
|
||||
>
|
||||
{isNudgingOverseer ? <Loader2 className="spin" aria-hidden="true" /> : <Send aria-hidden="true" />}
|
||||
@@ -4135,7 +4164,7 @@ export function TaskDetailContent({
|
||||
)}
|
||||
{(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff && !canNudgeOverseer && (
|
||||
<span className="detail-oversight-controls-helper" data-testid="detail-overseer-nudge-disabled-reason">
|
||||
{t("taskDetail.oversight.nudgeDisabledTitle", "Nudge unavailable: overseer is not actively watching this task")}
|
||||
{nudgeDisabledReason}
|
||||
</span>
|
||||
)}
|
||||
{(hasTaskOversightOverride || workflowOversightResolved) && showStopOverseer && (
|
||||
|
||||
@@ -140,7 +140,7 @@ describe("TaskDetailModal oversight controls", () => {
|
||||
expect(nudgeBtn).toBeDisabled();
|
||||
});
|
||||
|
||||
it("shows a visible group label and an in-DOM disabled-reason helper (not just a hover title) when Nudge is unavailable (FN-7546)", async () => {
|
||||
it("shows a visible group label and an in-DOM disabled-reason helper (not just a hover title) when Nudge is unavailable (FN-7546, reworded copy FN-7582)", async () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ id: "FN-111", column: "todo", plannerOversightLevel: "autonomous" })}
|
||||
@@ -159,8 +159,57 @@ describe("TaskDetailModal oversight controls", () => {
|
||||
const nudgeBtn = await screen.findByTestId("detail-overseer-nudge");
|
||||
expect(nudgeBtn).toBeDisabled();
|
||||
|
||||
// FN-7582: the old copy ("Nudge unavailable: overseer is not actively
|
||||
// watching this task") read as a fault report. The reworded copy must
|
||||
// frame the no-observation state as periodic/benign instead.
|
||||
const reason = await screen.findByTestId("detail-overseer-nudge-disabled-reason");
|
||||
expect(reason).toHaveTextContent("Nudge unavailable: overseer is not actively watching this task");
|
||||
expect(reason).not.toHaveTextContent("not actively watching this task");
|
||||
expect(reason).toHaveTextContent("Nudge becomes available once the overseer is observing this task's current stage");
|
||||
expect(nudgeBtn).toHaveAttribute("title", expect.stringContaining("Nudge becomes available once the overseer is observing this task's current stage"));
|
||||
});
|
||||
|
||||
it("desktop: shows the periodic-observation copy (not the old alarming phrase) for an in-progress task with no plannerOverseerState (FN-7582)", async () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ id: "FN-116", column: "in-progress", plannerOversightLevel: "autonomous" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
const nudgeBtn = await screen.findByTestId("detail-overseer-nudge");
|
||||
expect(nudgeBtn).toBeDisabled();
|
||||
|
||||
const reason = await screen.findByTestId("detail-overseer-nudge-disabled-reason");
|
||||
expect(reason).not.toHaveTextContent("not actively watching this task");
|
||||
expect(reason).toHaveTextContent("Nudge becomes available once the overseer is observing this task's current stage");
|
||||
expect(nudgeBtn).toHaveAttribute("title", expect.stringContaining("Nudge becomes available once the overseer is observing this task's current stage"));
|
||||
});
|
||||
|
||||
it("desktop: shows the human-control-suppressed copy (not the periodic-observation copy) when the task is user-paused (FN-7582)", async () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ id: "FN-117", column: "in-progress", plannerOversightLevel: "autonomous", plannerOverseerState: activeSnapshot, userPaused: true })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
const nudgeBtn = await screen.findByTestId("detail-overseer-nudge");
|
||||
expect(nudgeBtn).toBeDisabled();
|
||||
|
||||
const reason = await screen.findByTestId("detail-overseer-nudge-disabled-reason");
|
||||
expect(reason).toHaveTextContent("Nudge is paused while this task is under manual control.");
|
||||
expect(reason).not.toHaveTextContent("Nudge becomes available once the overseer is observing this task's current stage");
|
||||
expect(nudgeBtn).toHaveAttribute("title", expect.stringContaining("Nudge is paused while this task is under manual control."));
|
||||
});
|
||||
|
||||
it("does not show the disabled-reason helper when Nudge is enabled", async () => {
|
||||
@@ -453,6 +502,29 @@ describe("TaskDetailModal oversight controls — mobile breakpoint (FN-7521, FN-
|
||||
expect(await screen.findByTestId("detail-overseer-explain")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("still shows the reworded periodic-observation copy (not the old alarming phrase) behind the mobile overflow menu (FN-7582)", async () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({ id: "FN-216", column: "in-progress", plannerOversightLevel: "autonomous" })}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
await openOversightMenu();
|
||||
|
||||
const nudgeBtn = await screen.findByTestId("detail-overseer-nudge");
|
||||
expect(nudgeBtn).toBeDisabled();
|
||||
|
||||
const reason = await screen.findByTestId("detail-overseer-nudge-disabled-reason");
|
||||
expect(reason).not.toHaveTextContent("not actively watching this task");
|
||||
expect(reason).toHaveTextContent("Nudge becomes available once the overseer is observing this task's current stage");
|
||||
});
|
||||
|
||||
it("still renders no oversight-control leftover shell behind the mobile overflow menu for the off+inactive default case", async () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
|
||||
@@ -130,6 +130,13 @@ vi.mock("lucide-react", () => ({
|
||||
Bell: () => null,
|
||||
// FNXC:PlannerOversight 2026-07-04-19:00: FN-7545 mobile oversight overflow-menu trigger icon.
|
||||
MoreVertical: (props: any) => React.createElement("svg", { "data-testid": "more-vertical-icon", ...props }),
|
||||
// FNXC:Test 2026-07-05-11:20: FN-7579 added "ask-user"/"exit-gate" workflow node types to
|
||||
// WorkflowNodeTypes.tsx (HelpCircle, DoorOpen), which WorkflowNodeEditor/WorkflowResultsTab
|
||||
// import transitively behind TaskDetailModal's lazy workflow surfaces. The explicit mock list
|
||||
// omitted them, breaking every TaskDetailModal suite at import (pre-existing gap, unrelated to
|
||||
// FN-7582's copy change) — keep this list in sync with the node-editor icon set.
|
||||
HelpCircle: () => null,
|
||||
DoorOpen: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useAgentLogs", () => ({
|
||||
|
||||
Reference in New Issue
Block a user