diff --git a/.changeset/fn-7582-oversight-guideline-copy.md b/.changeset/fn-7582-oversight-guideline-copy.md
new file mode 100644
index 0000000000..4041f9f1c4
--- /dev/null
+++ b/.changeset/fn-7582-oversight-guideline-copy.md
@@ -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.
diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx
index e5c612c4b0..294362fe83 100644
--- a/packages/dashboard/app/components/TaskDetailModal.tsx
+++ b/packages/dashboard/app/components/TaskDetailModal.tsx
@@ -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 ? : }
@@ -4015,7 +4044,7 @@ export function TaskDetailContent({
)}
{!oversightIsOff && !canNudgeOverseer && (
- {t("taskDetail.oversight.nudgeDisabledTitle", "Nudge unavailable: overseer is not actively watching this task")}
+ {nudgeDisabledReason}
)}
{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 ? : }
@@ -4135,7 +4164,7 @@ export function TaskDetailContent({
)}
{(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff && !canNudgeOverseer && (
- {t("taskDetail.oversight.nudgeDisabledTitle", "Nudge unavailable: overseer is not actively watching this task")}
+ {nudgeDisabledReason}
)}
{(hasTaskOversightOverride || workflowOversightResolved) && showStopOverseer && (
diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx
index f403fbfb7b..768ed722a7 100644
--- a/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx
+++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.oversight-controls.test.tsx
@@ -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(
{
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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ 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(
({
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", () => ({