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:
7
.changeset/FN-7546-oversight-controls-clarity.md
Normal file
7
.changeset/FN-7546-oversight-controls-clarity.md
Normal 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.
|
||||
@@ -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-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.
|
||||
<!-- 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. -->
|
||||
- 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. -->
|
||||
|
||||
@@ -528,6 +528,35 @@ renders at a uniform height.
|
||||
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 {
|
||||
margin-top: var(--space-xs);
|
||||
padding: var(--space-sm);
|
||||
@@ -669,6 +698,19 @@ button.detail-oversight-menu-item svg {
|
||||
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:
|
||||
FN-7519 Intervention Timeline styles. Attaches to the closest existing seam
|
||||
|
||||
@@ -3873,6 +3873,11 @@ export function TaskDetailContent({
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
{!oversightIsOff && (
|
||||
<span className="detail-oversight-controls-label" data-testid="detail-oversight-controls-label">
|
||||
{t("taskDetail.oversight.controlsLabel", "Overseer controls")}
|
||||
</span>
|
||||
)}
|
||||
{!oversightIsOff && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -3892,6 +3897,11 @@ export function TaskDetailContent({
|
||||
<span>{t("taskDetail.oversight.nudge", "Nudge")}</span>
|
||||
</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 && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -3921,8 +3931,7 @@ export function TaskDetailContent({
|
||||
setShowOversightMenu(false);
|
||||
}}
|
||||
onKeyDown={handleOversightMenuKeyDown}
|
||||
disabled={!canExplainOverseer && !overseerExplainOpen}
|
||||
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")}
|
||||
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")}
|
||||
aria-label={t("taskDetail.oversight.explainAriaLabel", "Explain current action")}
|
||||
aria-expanded={overseerExplainOpen}
|
||||
>
|
||||
@@ -3971,7 +3980,27 @@ export function TaskDetailContent({
|
||||
silently vanishing; stop is hidden once oversight is already off
|
||||
(nothing left to stop) per the PROMPT's enablement rule, avoiding
|
||||
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 && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -3988,6 +4017,11 @@ export function TaskDetailContent({
|
||||
<span>{t("taskDetail.oversight.nudge", "Nudge")}</span>
|
||||
</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 && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -4011,8 +4045,7 @@ export function TaskDetailContent({
|
||||
onClick={() => {
|
||||
void handleExplainOverseer();
|
||||
}}
|
||||
disabled={!canExplainOverseer && !overseerExplainOpen}
|
||||
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")}
|
||||
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")}
|
||||
aria-label={t("taskDetail.oversight.explainAriaLabel", "Explain current action")}
|
||||
aria-expanded={overseerExplainOpen}
|
||||
>
|
||||
|
||||
@@ -140,6 +140,47 @@ 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 () => {
|
||||
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 () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
@@ -266,6 +307,34 @@ describe("TaskDetailModal oversight controls", () => {
|
||||
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 () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
|
||||
Reference in New Issue
Block a user