FN-8194: align task detail inline controls

Align task-detail actions with Quick Add while preserving existing integrations.

- Add inline attachment and eligible GitHub tracking controls
- Replace the Oversight menu dots with an Eye icon and reorder metadata actions
- Cover action order, integration behavior, icon rendering, and mobile wrapping

Files changed:
 .changeset/fn-8194-task-detail-inline-controls.md  |  7 ++
 docs/dashboard-guide.md                            |  6 +-
 .../dashboard/app/components/TaskDetailModal.tsx   | 99 +++++++++++++++-------
 ...lModal.inline-editing-and-integrations.test.tsx | 80 ++++++++++++++++-
 .../TaskDetailModal.mock-coverage.test.ts          |  1 +
 .../TaskDetailModal.oversight-controls.test.tsx    | 19 +++++
 ...etailModal.responsive-and-dependencies.test.tsx |  5 +-
 .../__tests__/TaskDetailModal.test-helpers.ts      | 10 +--
 8 files changed, 187 insertions(+), 40 deletions(-)

Fusion-Task-Id: FN-8194

Fusion-Task-Lineage: 8de0e36f-b428-401c-98aa-35964c556a39

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-17 00:55:57 -07:00
parent 3fdc2c1fb7
commit 87ffb24fca
8 changed files with 187 additions and 40 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Task detail action row now matches Quick Add — Eye icon for oversight, plus attach and GitHub-tracking buttons.
category: feature
dev: TaskDetailModal inline controls reordered to attach → GitHub → oversight(Eye) → priority → Fast; reuses existing upload and GitHub-tracking handlers. New test ids: detail-inline-attach, detail-inline-github-toggle.

View File

@@ -232,8 +232,10 @@ Features:
- Task cards show a compact **planner-overseer eye badge** (`data-testid="planner-overseer-state-badge"`) when the engine reports a non-idle transient `plannerOverseerState`. The eye badge is an active-overseer state marker, not a human-read/view indicator: `watching` means passive monitoring, `steering`/`recovering` mean active guidance or recovery is underway, and `awaiting-confirmation` means a human decision is required before the overseer can continue. Hover exposes the composed tooltip with the overseer's reason, watched stage/signal, and pending-confirmation note when present. - Task cards show a compact **planner-overseer eye badge** (`data-testid="planner-overseer-state-badge"`) when the engine reports a non-idle transient `plannerOverseerState`. The eye badge is an active-overseer state marker, not a human-read/view indicator: `watching` means passive monitoring, `steering`/`recovering` mean active guidance or recovery is underway, and `awaiting-confirmation` means a human decision is required before the overseer can continue. Hover exposes the composed tooltip with the overseer's reason, watched stage/signal, and pending-confirmation note when present.
<!-- 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.
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: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.
- The task detail modal's inline meta-controls cluster (next to Priority/Execution mode) exposes planner-overseer controls behind a single compact **"Oversight" overflow-menu button** (`data-testid="detail-oversight-menu-trigger"`, `MoreVertical` 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. -->
- 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: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. --> <!-- 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. - 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. <!-- 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

@@ -2,7 +2,7 @@ import "./TaskDetailModal.css";
import React, { Suspense, lazy, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import React, { Suspense, lazy, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch, ArrowLeft, Zap, Loader2, AlertTriangle, Sparkles, Maximize2, Minimize2, Send, Square, Info, MoreVertical, Eye, EyeOff } from "lucide-react"; import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch, ArrowLeft, Zap, Loader2, AlertTriangle, Sparkles, Maximize2, Minimize2, Send, Square, Info, Paperclip, Eye, EyeOff } from "lucide-react";
import { useModalResizePersist } from "../hooks/useModalResizePersist"; import { useModalResizePersist } from "../hooks/useModalResizePersist";
import { useMobileScrollLock } from "../hooks/useMobileScrollLock"; import { useMobileScrollLock } from "../hooks/useMobileScrollLock";
import { useOverlayDismiss } from "../hooks/useOverlayDismiss"; import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
@@ -4181,40 +4181,39 @@ export function TaskDetailContent({
</div> </div>
)} )}
<div className="detail-meta"> <div className="detail-meta">
{/*
FNXC:QuickAddActionRow 2026-07-16-16:00:
FN-8194: task-detail metadata mirrors Quick Add's action order:
attach, GitHub tracking, Oversight, Priority, then Fast. The compact
controls delegate to the existing single file-input upload and
GitHub-tracking handlers so this row never forks persistence paths.
*/}
<div className="detail-meta-inline-controls" data-testid="detail-meta-inline-controls"> <div className="detail-meta-inline-controls" data-testid="detail-meta-inline-controls">
<label
className={`card-priority-badge card-priority-badge--${inlinePriority} detail-priority-chip ${isSavingInlinePriority ? "detail-priority-chip--saving" : ""}`}
>
<span>{t("taskDetail.priority.label", "Priority:")}</span>
<select
className="detail-priority-select"
value={inlinePriority}
onChange={(event) => {
void handleInlinePriorityChange(event.target.value);
}}
disabled={isSavingInlinePriority}
aria-label={t("taskDetail.priority.ariaLabel", "Task priority")}
>
{TASK_PRIORITIES.map((priorityOption) => (
<option key={priorityOption} value={priorityOption}>
{priorityOption}
</option>
))}
</select>
</label>
<button <button
type="button" type="button"
className={`btn btn-sm detail-execution-mode-toggle ${inlineExecutionMode === "fast" ? "detail-execution-mode-toggle--fast" : ""} ${isSavingInlineExecutionMode ? "detail-execution-mode-toggle--saving" : ""}`} className="btn btn-icon btn-sm"
onClick={() => { data-testid="detail-inline-attach"
void handleInlineExecutionModeToggle(); onClick={() => fileInputRef.current?.click()}
}} disabled={uploading}
disabled={isSavingInlineExecutionMode} aria-label={t("taskDetail.attachments.attachInline", "Attach file")}
aria-label={t("taskDetail.executionMode.ariaLabel", "Execution mode: {{mode}}", { mode: inlineExecutionMode })} title={t("taskDetail.attachments.attachInline", "Attach file")}
aria-pressed={inlineExecutionMode === "fast"}
> >
<Zap aria-hidden="true" /> <Paperclip aria-hidden="true" />
<span>{inlineExecutionMode === "fast" ? t("taskDetail.executionMode.fast", "Fast") : t("taskDetail.executionMode.standard", "Standard")}</span>
</button> </button>
{canEditGithubTracking && !gitlabTrackedItem && (
<button
type="button"
className={`btn btn-icon btn-sm ${githubTrackingEnabled ? "btn-primary" : ""}`}
data-testid="detail-inline-github-toggle"
onClick={() => void handleToggleGithubTracking()}
disabled={isSavingGithubTracking}
aria-pressed={githubTrackingEnabled}
aria-label={t("taskDetail.githubTracking.toggleInline", "Toggle GitHub tracking")}
title={t("taskDetail.githubTracking.toggleInline", "Toggle GitHub tracking")}
>
<ProviderIcon provider="github" size="sm" />
</button>
)}
{/* {/*
FNXC:PlannerOversight 2026-07-04-17:00: FNXC:PlannerOversight 2026-07-04-17:00:
FN-7517 quick oversight-level-change control. Shows the current FN-7517 quick oversight-level-change control. Shows the current
@@ -4240,6 +4239,11 @@ export function TaskDetailContent({
stop / explain) render ONLY behind the "Oversight" overflow-menu stop / explain) render ONLY behind the "Oversight" overflow-menu
trigger on every surface (desktop and mobile); the former desktop trigger on every surface (desktop and mobile); the former desktop
inline cluster was removed for a consistent, simpler control bar. inline cluster was removed for a consistent, simpler control bar.
FNXC:PlannerOversight 2026-07-16-16:00:
FN-8194: use Eye for the Oversight overflow trigger so task detail
matches Quick Add's planner-advisor affordance without changing
the labeled menu's accessibility or behavior.
*/} */}
{(hasTaskOversightOverride || workflowOversightResolved) && ( {(hasTaskOversightOverride || workflowOversightResolved) && (
<div className="detail-oversight-menu-dropdown" ref={oversightMenuRef}> <div className="detail-oversight-menu-dropdown" ref={oversightMenuRef}>
@@ -4254,7 +4258,7 @@ export function TaskDetailContent({
aria-expanded={showOversightMenu} aria-expanded={showOversightMenu}
aria-label={t("taskDetail.oversight.menuAriaLabel", "Oversight actions")} aria-label={t("taskDetail.oversight.menuAriaLabel", "Oversight actions")}
> >
<MoreVertical aria-hidden="true" /> <Eye aria-hidden="true" />
<span>{t("taskDetail.oversight.menuLabel", "Oversight")}</span> <span>{t("taskDetail.oversight.menuLabel", "Oversight")}</span>
</button> </button>
{showOversightMenu && ( {showOversightMenu && (
@@ -4404,6 +4408,39 @@ export function TaskDetailContent({
)} )}
</div> </div>
)} )}
<label
className={`card-priority-badge card-priority-badge--${inlinePriority} detail-priority-chip ${isSavingInlinePriority ? "detail-priority-chip--saving" : ""}`}
>
<span>{t("taskDetail.priority.label", "Priority:")}</span>
<select
className="detail-priority-select"
value={inlinePriority}
onChange={(event) => {
void handleInlinePriorityChange(event.target.value);
}}
disabled={isSavingInlinePriority}
aria-label={t("taskDetail.priority.ariaLabel", "Task priority")}
>
{TASK_PRIORITIES.map((priorityOption) => (
<option key={priorityOption} value={priorityOption}>
{priorityOption}
</option>
))}
</select>
</label>
<button
type="button"
className={`btn btn-sm detail-execution-mode-toggle ${inlineExecutionMode === "fast" ? "detail-execution-mode-toggle--fast" : ""} ${isSavingInlineExecutionMode ? "detail-execution-mode-toggle--saving" : ""}`}
onClick={() => {
void handleInlineExecutionModeToggle();
}}
disabled={isSavingInlineExecutionMode}
aria-label={t("taskDetail.executionMode.ariaLabel", "Execution mode: {{mode}}", { mode: inlineExecutionMode })}
aria-pressed={inlineExecutionMode === "fast"}
>
<Zap aria-hidden="true" />
<span>{inlineExecutionMode === "fast" ? t("taskDetail.executionMode.fast", "Fast") : t("taskDetail.executionMode.standard", "Standard")}</span>
</button>
</div> </div>
{overseerExplainOpen && ( {overseerExplainOpen && (
<div className="detail-overseer-explain-panel" data-testid="detail-overseer-explain-panel" role="region" aria-live="polite"> <div className="detail-overseer-explain-panel" data-testid="detail-overseer-explain-panel" role="region" aria-live="polite">

View File

@@ -1207,7 +1207,12 @@ describe("TaskDetailModal", () => {
const prioritySelect = screen.getByRole("combobox", { name: "Task priority" }); const prioritySelect = screen.getByRole("combobox", { name: "Task priority" });
const executionModeToggle = screen.getByRole("button", { name: "Execution mode: standard" }); const executionModeToggle = screen.getByRole("button", { name: "Execution mode: standard" });
expect(prioritySelect.parentElement).toBe(controls.firstElementChild); /*
FNXC:QuickAddActionRow 2026-07-16-16:00:
FN-8194: attach, GitHub, and Oversight precede the Quick Add-matched
Priority/Fast controls; both controls remain direct interactive children.
*/
expect(controls).toContainElement(prioritySelect.parentElement);
expect(executionModeToggle.parentElement).toBe(controls); expect(executionModeToggle.parentElement).toBe(controls);
fireEvent.change(prioritySelect, { fireEvent.change(prioritySelect, {
@@ -3338,3 +3343,76 @@ describe("TaskDetailModal", () => {
}); });
}); });
}); });
describe("TaskDetailModal inline action row parity (FN-8194)", () => {
const renderDetail = (task: Task) => render(
<TaskDetailModal
initialTab="definition"
task={task}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
it("uses the existing file input and preserves Quick Add action order", async () => {
renderDetail(makeTask({ id: "FN-8194", column: "todo", plannerOversightLevel: "observe" }));
const controls = screen.getByTestId("detail-meta-inline-controls");
const attach = screen.getByTestId("detail-inline-attach");
const github = screen.getByTestId("detail-inline-github-toggle");
const oversight = await screen.findByTestId("detail-oversight-menu-trigger");
const priority = screen.getByRole("combobox", { name: "Task priority" }).parentElement!;
const fast = screen.getByRole("button", { name: "Execution mode: standard" });
const fileInput = document.querySelector<HTMLInputElement>('input[type="file"]')!;
const fileInputClick = vi.spyOn(fileInput, "click");
expect([...controls.children]).toEqual([attach, github, oversight.parentElement, priority, fast]);
fireEvent.click(attach);
expect(fileInputClick).toHaveBeenCalledOnce();
});
it("toggles GitHub tracking through the existing update path and reflects enabled state", async () => {
const { updateTask } = await import("../../api");
const mockUpdate = vi.mocked(updateTask);
mockUpdate.mockResolvedValueOnce(makeTask({ id: "FN-8194", column: "todo", githubTracking: { enabled: true } }) as Task);
renderDetail(makeTask({ id: "FN-8194", column: "todo", githubTracking: { enabled: false } }));
const toggle = screen.getByTestId("detail-inline-github-toggle");
expect(toggle).toHaveAttribute("aria-pressed", "false");
expect(toggle).not.toHaveClass("btn-primary");
fireEvent.click(toggle);
await waitFor(() => {
expect(mockUpdate).toHaveBeenCalledWith("FN-8194", { githubTracking: { enabled: true } }, undefined);
});
expect(toggle).toHaveAttribute("aria-pressed", "true");
expect(toggle).toHaveClass("btn-primary");
});
it("hides the GitHub toggle for non-editable and GitLab-tracked tasks", () => {
const { unmount } = renderDetail(makeTask({ id: "FN-8194", column: "done", githubTracking: { enabled: true } }));
expect(screen.queryByTestId("detail-inline-github-toggle")).not.toBeInTheDocument();
unmount();
renderDetail(makeTask({
id: "FN-8195",
column: "todo",
gitlabTracking: {
item: {
kind: "project_issue",
projectPath: "acme/app",
iid: 1,
title: "GitLab issue",
url: "https://gitlab.com/acme/app/-/issues/1",
state: "opened",
},
},
}) as Task);
expect(screen.queryByTestId("detail-inline-github-toggle")).not.toBeInTheDocument();
});
});

View File

@@ -14,6 +14,7 @@ describe("TaskDetailModal lucide mock coverage", () => {
expect(lucideMock.ChevronRight).toBeDefined(); expect(lucideMock.ChevronRight).toBeDefined();
expect(lucideMock.ChevronUp).toBeDefined(); expect(lucideMock.ChevronUp).toBeDefined();
expect(lucideMock.Maximize2).toBeDefined(); expect(lucideMock.Maximize2).toBeDefined();
expect(lucideMock.Paperclip).toBeDefined();
expect(lucideMock.Pencil).toBeDefined(); expect(lucideMock.Pencil).toBeDefined();
expect(lucideMock.Workflow).toBeDefined(); expect(lucideMock.Workflow).toBeDefined();
expect(lucideMock.X).toBeDefined(); expect(lucideMock.X).toBeDefined();

View File

@@ -59,6 +59,25 @@ async function openOversightMenu() {
} }
describe("TaskDetailModal oversight controls", () => { describe("TaskDetailModal oversight controls", () => {
it("uses the Eye icon for the labeled oversight overflow trigger", async () => {
render(
<TaskDetailModal
task={makeTask({ id: "FN-8194", column: "in-progress", plannerOversightLevel: "observe" })}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
const trigger = await screen.findByTestId("detail-oversight-menu-trigger");
expect(trigger).toHaveTextContent("Oversight");
expect(trigger.querySelector('[data-testid="eye-icon"]')).toBeInTheDocument();
expect(trigger.querySelector('[data-testid="more-vertical-icon"]')).not.toBeInTheDocument();
});
beforeEach(async () => { beforeEach(async () => {
vi.clearAllMocks(); vi.clearAllMocks();
mockConfirm.mockResolvedValue(true); mockConfirm.mockResolvedValue(true);

View File

@@ -242,11 +242,14 @@ describe("TaskDetailModal", () => {
expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-timestamps\s*\{[^}]*flex-direction:\s*column;/); expect(css).not.toMatch(/@media[^{]*\(max-width: 768px\)[^{]*\{[\s\S]*?\.detail-timestamps\s*\{[^}]*flex-direction:\s*column;/);
}); });
it("keeps inline metadata controls in a single row without a narrow-screen column fallback", () => { it("keeps inline metadata controls in a single row with a wrapping mobile fallback", () => {
const css = readDashboardStylesSource(); const css = readDashboardStylesSource();
const mobileBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".detail-meta-inline-controls");
expectBaseRule(css, ".detail-meta-inline-controls", "display: flex;"); expectBaseRule(css, ".detail-meta-inline-controls", "display: flex;");
expectBaseRule(css, ".detail-meta-inline-controls", "flex-wrap: nowrap;"); expectBaseRule(css, ".detail-meta-inline-controls", "flex-wrap: nowrap;");
expect(mobileBlock).toMatch(/\.detail-meta-inline-controls\s*\{[^}]*flex-wrap:\s*wrap;/);
expect(mobileBlock).not.toMatch(/\.detail-meta-inline-controls\s*\{[^}]*flex-direction:\s*column;/);
expect(css).not.toMatch(/@media \(max-width: 640px\)\s*\{[^}]*\.detail-meta-inline-controls\s*\{[^}]*flex-direction:\s*column;/); expect(css).not.toMatch(/@media \(max-width: 640px\)\s*\{[^}]*\.detail-meta-inline-controls\s*\{[^}]*flex-direction:\s*column;/);
}); });

View File

@@ -132,13 +132,13 @@ vi.mock("lucide-react", () => ({
Code2: () => null, Code2: () => null,
Cpu: () => null, Cpu: () => null,
Bell: () => null, 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:DashboardTests 2026-07-15-11:55: FNXC:DashboardTests 2026-07-16-16:00:
Session-advisor toggle on TaskDetailContent uses Eye/EyeOff; missing exports break every FN-8194 replaces the Task Detail oversight dots with Eye and adds Paperclip
TaskDetailModal suite that mounts the session-advisor control (including oversight-mobile). to its inline action row. Keep both exports in the shared modal mock so every
focused TaskDetailModal suite mounts the production affordances.
*/ */
Paperclip: (props: any) => React.createElement("svg", { "data-testid": "paperclip-icon", ...props }),
Eye: (props: any) => React.createElement("svg", { "data-testid": "eye-icon", ...props }), Eye: (props: any) => React.createElement("svg", { "data-testid": "eye-icon", ...props }),
EyeOff: (props: any) => React.createElement("svg", { "data-testid": "eye-off-icon", ...props }), EyeOff: (props: any) => React.createElement("svg", { "data-testid": "eye-off-icon", ...props }),
// FNXC:Test 2026-07-05-11:20: FN-7579 added "ask-user"/"exit-gate" workflow node types to // FNXC:Test 2026-07-05-11:20: FN-7579 added "ask-user"/"exit-gate" workflow node types to