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:
@@ -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.
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user