FN-7546: clarify planner-oversight Nudge/Explain controls with visible label and in-DOM disabled reason

Add a visible group label and an in-DOM disabled-reason helper to the task-detail oversight controls so the Nudge/Explain cluster is no longer confusing when greyed out, and make Explain always-openable since it is read-only.

- Add a `detail-oversight-controls-label` group label above the Nudge/Stop/Explain buttons, gated by the same visibility condition as the buttons (mobile and desktop clusters)
- Add a `detail-overseer-nudge-disabled-reason` in-DOM helper line explaining why Nudge is unavailable, instead of relying only on a hover title
- Remove the disabled gate from Explain (it's read-only/non-mutating) and update its title copy to reflect that it always opens and shows last-known state when inactive
- Add regression tests covering the new label/helper text and Explain's always-enabled behavior
- Add a patch changeset and a docs/dashboard-guide.md note

Files changed:
 .changeset/FN-7546-oversight-controls-clarity.md   |  7 +++
 docs/dashboard-guide.md                            |  2 +
 .../dashboard/app/components/TaskDetailModal.css   | 42 +++++++++++++
 .../dashboard/app/components/TaskDetailModal.tsx   | 41 +++++++++++--
 .../TaskDetailModal.oversight-controls.test.tsx    | 69 ++++++++++++++++++++++
 5 files changed, 157 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-7546

Fusion-Task-Lineage: d1f342ea-2de0-4b54-9930-9b3d540c7af6

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-04 20:24:29 -07:00
parent 883c73e2f1
commit 3d55102091
5 changed files with 157 additions and 4 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Clarify the task-detail planner-oversight Nudge/Explain controls with a visible label, an in-DOM disabled reason, and an always-openable Explain panel.
category: fix
dev: TaskDetailModal now renders a `detail-oversight-controls-label` group label and `detail-overseer-nudge-disabled-reason` helper text (both gated by the existing oversight-cluster visibility condition); Explain no longer disables on `!canExplainOverseer` since it is read-only. Nudge's `canNudgeOverseer` gate and Stop's confirm dialog are unchanged.

View File

@@ -198,6 +198,8 @@ Features:
<!-- FNXC:PlannerOversight 2026-07-04-HH:MM: FN-7542 removed the FN-7516 active-overseer-state ("Executor") indicator described above as unwanted per-card noise — it fired on nearly every in-progress card. The oversight-level badge documented above is unaffected. --> <!-- FNXC:PlannerOversight 2026-07-04-HH:MM: FN-7542 removed the FN-7516 active-overseer-state ("Executor") indicator described above as unwanted per-card noise — it fired on nearly every in-progress card. The oversight-level badge documented above is unaffected. -->
<!-- FNXC:PlannerOversight 2026-07-04-17:00: FN-7517 adds interactive task-detail planner-overseer controls (quick level change, manual nudge, stop oversight, explain current action) alongside the FN-7516 read-only card badges above. These controls live ONLY in TaskDetailModal, not TaskCard. --> <!-- FNXC:PlannerOversight 2026-07-04-17:00: FN-7517 adds interactive task-detail planner-overseer controls (quick level change, manual nudge, stop oversight, explain current action) alongside the FN-7516 read-only card badges above. These controls live ONLY in TaskDetailModal, not TaskCard. -->
- The task detail modal's inline meta-controls cluster (next to Priority/Execution mode) adds four planner-overseer controls: a **quick oversight-level select** (`data-testid="detail-oversight-level-select"`) that writes the per-task `plannerOversightLevel` override (Off/Observe/Steer/Autonomous recovery) or clears it back to the inherited workflow/project default via an "Inherit" option; a **manual nudge** button (`data-testid="detail-overseer-nudge"`) that asks the overseer to inject one guidance-only steering comment into the currently watched stage right now (never a merge/PR/destructive action), disabled when the overseer is off/inactive or the task is user-paused/done/archived/`autoMerge:false` in-review; a **stop oversight** button (`data-testid="detail-overseer-stop"`) that disables active oversight for the task (confirmation-gated), hidden once oversight is already off; and an **explain current action** button (`data-testid="detail-overseer-explain"`) that toggles a small read-only panel (`data-testid="detail-overseer-explain-panel"`) showing the overseer's watched stage, reason, last action, and attempt count/limit, with a non-empty-shell inactive state when the overseer is not currently watching. All three action controls call the `POST /tasks/:id/overseer/nudge`, `POST /tasks/:id/overseer/stop`, and `GET /tasks/:id/overseer/explain` routes. - The task detail modal's inline meta-controls cluster (next to Priority/Execution mode) adds four planner-overseer controls: a **quick oversight-level select** (`data-testid="detail-oversight-level-select"`) that writes the per-task `plannerOversightLevel` override (Off/Observe/Steer/Autonomous recovery) or clears it back to the inherited workflow/project default via an "Inherit" option; a **manual nudge** button (`data-testid="detail-overseer-nudge"`) that asks the overseer to inject one guidance-only steering comment into the currently watched stage right now (never a merge/PR/destructive action), disabled when the overseer is off/inactive or the task is user-paused/done/archived/`autoMerge:false` in-review; a **stop oversight** button (`data-testid="detail-overseer-stop"`) that disables active oversight for the task (confirmation-gated), hidden once oversight is already off; and an **explain current action** button (`data-testid="detail-overseer-explain"`) that toggles a small read-only panel (`data-testid="detail-overseer-explain-panel"`) showing the overseer's watched stage, reason, last action, and attempt count/limit, with a non-empty-shell inactive state when the overseer is not currently watching. All three action controls call the `POST /tasks/:id/overseer/nudge`, `POST /tasks/:id/overseer/stop`, and `GET /tasks/:id/overseer/explain` routes.
<!-- FNXC:PlannerOversight 2026-07-04-20:30: FN-7546 clarifies the cluster above — operators reported the buttons were unlabeled and looked inert, with only a hover title explaining why. Adds a visible group label and an always-visible disabled-reason line, and makes Explain always openable since it never mutates anything. -->
- The FN-7517 controls above carry a visible, non-interactive **`"Overseer controls"` group label** (`data-testid="detail-oversight-controls-label"`) so Nudge/Stop/Explain read as an identifiable cluster rather than unlabeled chips; the label is gated by the same `(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff` condition as the buttons, so it never renders an empty shell when oversight is Off/unresolved. When **Nudge** is disabled, an always-visible helper line (`data-testid="detail-overseer-nudge-disabled-reason"`) states the reason in-DOM (mirroring the existing hover `title`) instead of relying on a mouse-hover tooltip alone. **Explain** is read-only and non-mutating, so it is never disabled purely because the overseer is inactive — clicking it always opens/closes the panel, which shows the overseer's live state when watching or an informative "not currently watching this task" message otherwise. Nudge's mutating enablement rule (`canNudgeOverseer`, including the human-control suppression cases) and Stop's confirmation dialog are unchanged.
<!-- FNXC:PlannerOversight 2026-07-04-19:00: FN-7545 collapses the above controls into a mobile overflow menu below the 768px breakpoint. --> <!-- FNXC:PlannerOversight 2026-07-04-19:00: FN-7545 collapses the above controls into a mobile overflow menu below the 768px breakpoint. -->
- On mobile (viewport ≤ 768px), the level-select/nudge/stop/explain controls above collapse behind a single compact **"Oversight" overflow-menu button** (`data-testid="detail-oversight-menu-trigger"`, `MoreVertical` icon, `aria-haspopup="menu"`) that opens a `role="menu"` popover (mirroring the existing move-action dropdown pattern) listing the same controls as full-width, comfortably tappable `role="menuitem"` entries with the SAME testids and enablement/visibility rules as the desktop inline controls. The trigger itself is withheld entirely when none of those controls would render (no empty-shell overflow button for the oversight-off + overseer-inactive default). Desktop (>768px) keeps the inline cluster unchanged. - On mobile (viewport ≤ 768px), the level-select/nudge/stop/explain controls above collapse behind a single compact **"Oversight" overflow-menu button** (`data-testid="detail-oversight-menu-trigger"`, `MoreVertical` icon, `aria-haspopup="menu"`) that opens a `role="menu"` popover (mirroring the existing move-action dropdown pattern) listing the same controls as full-width, comfortably tappable `role="menuitem"` entries with the SAME testids and enablement/visibility rules as the desktop inline controls. The trigger itself is withheld entirely when none of those controls would render (no empty-shell overflow button for the oversight-off + overseer-inactive default). Desktop (>768px) keeps the inline cluster unchanged.
<!-- FNXC:PlannerOversight 2026-07-04-18:00: FN-7519 adds a read-only Intervention Timeline immediately below the FN-7517 controls above. --> <!-- FNXC:PlannerOversight 2026-07-04-18:00: FN-7519 adds a read-only Intervention Timeline immediately below the FN-7517 controls above. -->

View File

@@ -528,6 +528,35 @@ renders at a uniform height.
opacity: 0.5; opacity: 0.5;
} }
/*
FNXC:PlannerOversight 2026-07-04-20:30:
FN-7546 — group label + always-visible disabled-reason helper for the
Nudge/Stop/Explain cluster, so operators no longer rely on a mouse-hover
`title` alone to understand what the controls are or why Nudge is inert.
Both are gated by the same condition as the buttons themselves (never an
empty shell when oversight is Off/unresolved). Uses the same muted-text /
small-caption convention as `.detail-overseer-explain-panel__empty` rather
than a one-off style.
*/
.detail-oversight-controls-label {
display: inline-flex;
align-items: center;
min-height: var(--detail-priority-control-min-height);
color: var(--text-muted);
font-size: var(--font-size-sm, 0.875rem);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.02em;
}
.detail-oversight-controls-helper {
display: inline-flex;
align-items: center;
flex-basis: 100%;
color: var(--text-muted);
font-size: var(--font-size-sm, 0.875rem);
}
.detail-overseer-explain-panel { .detail-overseer-explain-panel {
margin-top: var(--space-xs); margin-top: var(--space-xs);
padding: var(--space-sm); padding: var(--space-sm);
@@ -669,6 +698,19 @@ button.detail-oversight-menu-item svg {
height: 1em; height: 1em;
} }
/*
FNXC:PlannerOversight 2026-07-04-20:30:
FN-7546 — the same group label / disabled-reason helper as the desktop
cluster above, adapted to the mobile overflow-menu's padded list-item layout
so they read as ordinary (non-interactive) menu rows rather than floating
text.
*/
.detail-oversight-menu .detail-oversight-controls-label,
.detail-oversight-menu .detail-oversight-controls-helper {
padding: var(--space-xs) var(--space-md);
min-height: 0;
}
/* /*
FNXC:PlannerOversight 2026-07-04-18:00: FNXC:PlannerOversight 2026-07-04-18:00:
FN-7519 Intervention Timeline styles. Attaches to the closest existing seam FN-7519 Intervention Timeline styles. Attaches to the closest existing seam

View File

@@ -3873,6 +3873,11 @@ export function TaskDetailContent({
))} ))}
</select> </select>
</label> </label>
{!oversightIsOff && (
<span className="detail-oversight-controls-label" data-testid="detail-oversight-controls-label">
{t("taskDetail.oversight.controlsLabel", "Overseer controls")}
</span>
)}
{!oversightIsOff && ( {!oversightIsOff && (
<button <button
type="button" type="button"
@@ -3892,6 +3897,11 @@ export function TaskDetailContent({
<span>{t("taskDetail.oversight.nudge", "Nudge")}</span> <span>{t("taskDetail.oversight.nudge", "Nudge")}</span>
</button> </button>
)} )}
{!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")}
</span>
)}
{showStopOverseer && ( {showStopOverseer && (
<button <button
type="button" type="button"
@@ -3921,8 +3931,7 @@ export function TaskDetailContent({
setShowOversightMenu(false); setShowOversightMenu(false);
}} }}
onKeyDown={handleOversightMenuKeyDown} onKeyDown={handleOversightMenuKeyDown}
disabled={!canExplainOverseer && !overseerExplainOpen} title={canExplainOverseer ? t("taskDetail.oversight.explainTitle", "Explain the overseer's current action") : t("taskDetail.oversight.explainInactiveTitle", "Overseer is not currently watching this task — Explain shows its last known state")}
title={canExplainOverseer ? t("taskDetail.oversight.explainTitle", "Explain the overseer's current action") : t("taskDetail.oversight.explainDisabledTitle", "Explain unavailable: overseer is not actively watching this task")}
aria-label={t("taskDetail.oversight.explainAriaLabel", "Explain current action")} aria-label={t("taskDetail.oversight.explainAriaLabel", "Explain current action")}
aria-expanded={overseerExplainOpen} aria-expanded={overseerExplainOpen}
> >
@@ -3971,7 +3980,27 @@ export function TaskDetailContent({
silently vanishing; stop is hidden once oversight is already off silently vanishing; stop is hidden once oversight is already off
(nothing left to stop) per the PROMPT's enablement rule, avoiding (nothing left to stop) per the PROMPT's enablement rule, avoiding
an always-on empty shell for the common oversight-off default. an always-on empty shell for the common oversight-off default.
FNXC:PlannerOversight 2026-07-04-20:30:
FN-7546 — operators reported these controls were confusing:
unlabeled inline chips, greyed out most of the time, with the
only "why" behind a mouse-hover `title`. Add a visible,
non-interactive `detail-oversight-controls-label` group label
(gated by the SAME condition as the buttons) so the cluster is
identifiable, and a `detail-overseer-nudge-disabled-reason`
helper line that surfaces the disabled reason in-DOM (not just
on hover) whenever Nudge is unavailable. Explain is read-only
and non-mutating, so its disabled gate is removed entirely
(see handleExplainOverseer below) — it always opens its panel,
which already renders an informative "not currently watching"
empty state. Nudge's mutating gate (`canNudgeOverseer`) and
Stop's confirm dialog are unchanged.
*/} */}
{(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff && (
<span className="detail-oversight-controls-label" data-testid="detail-oversight-controls-label">
{t("taskDetail.oversight.controlsLabel", "Overseer controls")}
</span>
)}
{(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff && ( {(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff && (
<button <button
type="button" type="button"
@@ -3988,6 +4017,11 @@ export function TaskDetailContent({
<span>{t("taskDetail.oversight.nudge", "Nudge")}</span> <span>{t("taskDetail.oversight.nudge", "Nudge")}</span>
</button> </button>
)} )}
{(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")}
</span>
)}
{(hasTaskOversightOverride || workflowOversightResolved) && showStopOverseer && ( {(hasTaskOversightOverride || workflowOversightResolved) && showStopOverseer && (
<button <button
type="button" type="button"
@@ -4011,8 +4045,7 @@ export function TaskDetailContent({
onClick={() => { onClick={() => {
void handleExplainOverseer(); void handleExplainOverseer();
}} }}
disabled={!canExplainOverseer && !overseerExplainOpen} title={canExplainOverseer ? t("taskDetail.oversight.explainTitle", "Explain the overseer's current action") : t("taskDetail.oversight.explainInactiveTitle", "Overseer is not currently watching this task — Explain shows its last known state")}
title={canExplainOverseer ? t("taskDetail.oversight.explainTitle", "Explain the overseer's current action") : t("taskDetail.oversight.explainDisabledTitle", "Explain unavailable: overseer is not actively watching this task")}
aria-label={t("taskDetail.oversight.explainAriaLabel", "Explain current action")} aria-label={t("taskDetail.oversight.explainAriaLabel", "Explain current action")}
aria-expanded={overseerExplainOpen} aria-expanded={overseerExplainOpen}
> >

View File

@@ -140,6 +140,47 @@ describe("TaskDetailModal oversight controls", () => {
expect(nudgeBtn).toBeDisabled(); 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 () => {
render(
<TaskDetailModal
task={makeTask({ id: "FN-111", column: "todo", plannerOversightLevel: "autonomous" })}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
const label = await screen.findByTestId("detail-oversight-controls-label");
expect(label).toHaveTextContent("Overseer controls");
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 unavailable: overseer is not actively watching this task");
});
it("does not show the disabled-reason helper when Nudge is enabled", async () => {
render(
<TaskDetailModal
task={makeTask({ id: "FN-112", column: "in-progress", plannerOversightLevel: "autonomous", plannerOverseerState: activeSnapshot })}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
const nudgeBtn = await screen.findByTestId("detail-overseer-nudge");
expect(nudgeBtn).not.toBeDisabled();
expect(screen.queryByTestId("detail-overseer-nudge-disabled-reason")).not.toBeInTheDocument();
});
it("nudge is disabled while the task is user-paused", async () => { it("nudge is disabled while the task is user-paused", async () => {
render( render(
<TaskDetailModal <TaskDetailModal
@@ -266,6 +307,34 @@ describe("TaskDetailModal oversight controls", () => {
expect(panel).toHaveTextContent("not currently watching"); expect(panel).toHaveTextContent("not currently watching");
}); });
it("Explain is never disabled while the overseer is inactive and always opens the read-only panel (FN-7546)", async () => {
const api = await import("../../api");
vi.mocked(api.explainOverseer).mockResolvedValueOnce({ snapshot: null });
render(
<TaskDetailModal
task={makeTask({ id: "FN-113", column: "todo", plannerOversightLevel: "autonomous" })}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
const explainBtn = await screen.findByTestId("detail-overseer-explain");
// Read-only Explain must never be disabled purely because the overseer
// isn't actively watching — that inactive state is exactly what the
// panel's empty-state message communicates.
expect(explainBtn).not.toBeDisabled();
fireEvent.click(explainBtn);
const panel = await screen.findByTestId("detail-overseer-explain-panel");
expect(panel).toHaveTextContent("not currently watching");
});
it("renders no oversight-control leftover shell when oversight is off and the overseer is inactive (default case)", async () => { it("renders no oversight-control leftover shell when oversight is off and the overseer is inactive (default case)", async () => {
render( render(
<TaskDetailModal <TaskDetailModal