FN-8233: reflect oversight state in menu trigger

Make the Oversight menu trigger icon reflect the effective overseer configuration.

- Switch the trigger between Eye and EyeOff based on oversight level and Session advisor state.
- Cover icon updates after changing either control.
- Document the combined trigger-state behavior.

Files changed:
 docs/dashboard-guide.md                            |  6 ++-
 .../dashboard/app/components/TaskDetailModal.tsx   | 10 +++-
 .../TaskDetailModal.oversight-controls.test.tsx    | 61 ++++++++++++++++++++++
 3 files changed, 74 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-8233

Fusion-Task-Lineage: 16012bf1-94f8-4461-90b6-95d309aa9a6d

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-17 13:38:32 -07:00
parent 2f7da57601
commit f0b6e4be0c
3 changed files with 74 additions and 3 deletions

View File

@@ -235,8 +235,10 @@ Features:
FNXC:PlannerOversight 2026-07-05-00:00: FN-7604 collapses the desktop inline cluster (documented below through FN-7545/FN-7546) into the single universal overflow-menu dropdown that FN-7545 originally built for mobile only — the dropdown is now the ONE canonical surface on every viewport, desktop included.
FNXC:QuickAddActionRow 2026-07-16-16:00: FN-8194 aligns task-detail metadata actions with Quick Add: attach, GitHub tracking, Oversight (Eye), Priority, then Fast. -->
- The task detail modal's inline meta-controls cluster follows Quick Add order: compact **Attach file** (`data-testid="detail-inline-attach"`), eligible-task **GitHub tracking** toggle (`data-testid="detail-inline-github-toggle"`), **Oversight**, Priority, and Execution mode. Attach opens the existing task attachment picker; GitHub toggles the existing tracking setting and is omitted for GitLab-tracked or non-editable tasks. The Oversight control exposes planner-overseer actions behind a compact **"Oversight" overflow-menu button** (`data-testid="detail-oversight-menu-trigger"`, `Eye` icon, `aria-haspopup="menu"`) on every viewport, desktop and mobile alike. Clicking the trigger opens a `role="menu"` popover (mirroring the existing move-action dropdown pattern) containing: 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 trigger itself is withheld entirely when none of the menu's controls would render (no empty-shell overflow button for the unresolved-default case).
FNXC:QuickAddActionRow 2026-07-16-16:00: FN-8194 aligns task-detail metadata actions with Quick Add: attach, GitHub tracking, Oversight (Eye), Priority, then Fast.
FNXC:PlannerOversight 2026-07-17-13:18: FN-8233 makes the task-detail Oversight trigger communicate the combined effective overseer state: it uses Eye when the oversight level is active or the Session advisor is enabled, and EyeOff only when both are off. -->
- The task detail modal's inline meta-controls cluster follows Quick Add order: compact **Attach file** (`data-testid="detail-inline-attach"`), eligible-task **GitHub tracking** toggle (`data-testid="detail-inline-github-toggle"`), **Oversight**, Priority, and Execution mode. Attach opens the existing task attachment picker; GitHub toggles the existing tracking setting and is omitted for GitLab-tracked or non-editable tasks. The Oversight control exposes planner-overseer actions behind a compact **"Oversight" overflow-menu button** (`data-testid="detail-oversight-menu-trigger"`, `Eye` when the oversight level is active or the Session advisor is enabled, otherwise `EyeOff`, `aria-haspopup="menu"`) on every viewport, desktop and mobile alike. Clicking the trigger opens a `role="menu"` popover (mirroring the existing move-action dropdown pattern) containing: 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 trigger itself is withheld entirely when none of the menu's controls would render (no empty-shell overflow button for the unresolved-default case).
<!-- 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 controls inside the Oversight menu 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 entries; the label is gated by the same `(hasTaskOversightOverride || workflowOversightResolved) && !oversightIsOff` condition as the buttons, so it never renders when oversight is Off/unresolved (opening the menu in that state shows only the level select). 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 originally introduced this overflow menu for mobile only, collapsing the level-select/nudge/stop/explain controls behind the trigger below the 768px breakpoint while desktop kept an inline cluster.

View File

@@ -3387,6 +3387,14 @@ export function TaskDetailContent({
const isOverseerHumanReviewTerminal = task.column === "in-review" && !effectiveAutoMerge;
const overseerHumanControlSuppressed = Boolean(isTaskPaused) || isDoneOrArchivedColumn || isOverseerHumanReviewTerminal;
const oversightIsOff = effectiveOversightLevel === "off";
/*
FNXC:PlannerOversight 2026-07-17-13:18:
FN-8233: the Oversight trigger must visibly track effective overseer state.
`overseerTriggerOn = !oversightIsOff || effectiveSessionAdvisorEnabled` lets
either the level select or Session advisor toggle in its dropdown update the
shared trigger icon without introducing a stale local state snapshot.
*/
const overseerTriggerOn = !oversightIsOff || effectiveSessionAdvisorEnabled;
const canNudgeOverseer = overseerActive && !oversightIsOff && !overseerHumanControlSuppressed;
const canExplainOverseer = overseerActive && !oversightIsOff;
const showStopOverseer = !oversightIsOff;
@@ -4273,7 +4281,7 @@ export function TaskDetailContent({
aria-label={t("taskDetail.oversight.menuAriaLabel", "Oversight actions")}
title={t("taskDetail.oversight.menuAriaLabel", "Oversight actions")}
>
<Eye aria-hidden="true" />
{overseerTriggerOn ? <Eye aria-hidden="true" /> : <EyeOff aria-hidden="true" />}
</button>
{showOversightMenu && (
<div className="detail-oversight-menu" role="menu" onKeyDown={handleOversightMenuKeyDown}>

View File

@@ -81,6 +81,67 @@ describe("TaskDetailModal oversight controls", () => {
expect(trigger.querySelector('[data-testid="more-vertical-icon"]')).not.toBeInTheDocument();
});
it("updates the trigger icon for oversight-level and session-advisor predicate changes", async () => {
const api = await import("../../api");
const mockUpdate = vi.mocked(api.updateTask);
let currentTask = makeTask({
id: "FN-8233",
column: "in-progress",
plannerOversightLevel: "observe",
sessionAdvisorEnabled: false,
});
mockUpdate.mockImplementation(async (_id, patch) => {
currentTask = makeTask({ ...currentTask, ...patch });
return currentTask as any;
});
let rerenderModal: (task: typeof currentTask) => void;
const renderModal = (task: typeof currentTask) => (
<TaskDetailModal
task={task}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
onTaskUpdated={(updatedTask) => rerenderModal(updatedTask as typeof currentTask)}
addToast={noop}
/>
);
const rendered = render(renderModal(currentTask));
rerenderModal = (updatedTask) => rendered.rerender(renderModal(updatedTask));
const trigger = await screen.findByTestId("detail-oversight-menu-trigger");
expect(trigger.querySelector('[data-testid="eye-icon"]')).toBeInTheDocument();
await openOversightMenu();
const select = await screen.findByTestId("detail-oversight-level-select");
fireEvent.change(select, { target: { value: "off" } });
await waitFor(() => {
expect(trigger.querySelector('[data-testid="eye-off-icon"]')).toBeInTheDocument();
});
fireEvent.change(select, { target: { value: "observe" } });
await waitFor(() => {
expect(trigger.querySelector('[data-testid="eye-icon"]')).toBeInTheDocument();
});
fireEvent.change(select, { target: { value: "off" } });
await waitFor(() => {
expect(trigger.querySelector('[data-testid="eye-off-icon"]')).toBeInTheDocument();
});
fireEvent.click(await screen.findByTestId("detail-session-advisor-toggle"));
await waitFor(() => {
expect(trigger.querySelector('[data-testid="eye-icon"]')).toBeInTheDocument();
});
fireEvent.click(await screen.findByTestId("detail-session-advisor-toggle"));
await waitFor(() => {
expect(trigger.querySelector('[data-testid="eye-off-icon"]')).toBeInTheDocument();
});
});
beforeEach(async () => {
vi.clearAllMocks();
mockConfirm.mockResolvedValue(true);