FN-7331: remove Quick Add AI refine control

Remove the Quick Add refine affordance while preserving richer task refinement elsewhere.

- Drop QuickEntryBox refine state, menu positioning, API calls, and action rendering.
- Update Quick Add tests to assert the refine controls and refine-text path are absent.
- Add a patch changeset for the published CLI bundle.

Files changed:
 .changeset/fn-7331-remove-quick-add-refine.md      |   7 +
 .../__tests__/quick-entry-expanded-height.test.tsx |   3 +-
 .../dashboard/app/components/QuickEntryBox.tsx     | 198 +-------------
 .../components/__tests__/QuickEntryBox.test.tsx    | 286 ++++-----------------
 4 files changed, 71 insertions(+), 423 deletions(-)

Fusion-Task-Id: FN-7331

Fusion-Task-Lineage: ed30e9da-1c1c-4e94-9fbe-e44ad5ae7133

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-30 21:07:35 -07:00
parent a48ff305db
commit aa4c2c1b26
4 changed files with 73 additions and 425 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Remove the AI Refine action from Quick Add task creation.
category: fix
dev: QuickEntryBox no longer renders or calls the refine-text path; TaskForm refine remains available.

View File

@@ -40,9 +40,8 @@ vi.mock("../api", () => ({
groupOverlappingFiles: true,
autoMerge: true,
}),
refineText: vi.fn(),
getRefineErrorMessage: vi.fn((err: unknown) => (err as Error)?.message || "Failed"),
fetchAgents: vi.fn().mockResolvedValue([]),
fetchWorkflowOptionalSteps: vi.fn().mockResolvedValue([]),
uploadAttachment: vi.fn().mockResolvedValue({}),
updateGlobalSettings: vi.fn().mockResolvedValue({}),
}));

View File

@@ -5,8 +5,8 @@ import { createPortal } from "react-dom";
import type { ToastType } from "../hooks/useToast";
import { DEFAULT_TASK_PRIORITY, TASK_PRIORITIES, getErrorMessage } from "@fusion/core";
import type { Task, Settings, TaskPriority, ResolvedWorkflowOptionalStep } from "@fusion/core";
import type { ModelInfo, RefinementType, Agent, CreateTaskInput, DuplicateMatch, BoardWorkflowDefinition, NodeInfo } from "../api";
import { checkDuplicateTasks, fetchModels, fetchSettings, refineText, getRefineErrorMessage, updateGlobalSettings, fetchAgents, uploadAttachment, fetchWorkflowOptionalSteps } from "../api";
import type { ModelInfo, Agent, CreateTaskInput, DuplicateMatch, BoardWorkflowDefinition, NodeInfo } from "../api";
import { checkDuplicateTasks, fetchModels, fetchSettings, updateGlobalSettings, fetchAgents, uploadAttachment, fetchWorkflowOptionalSteps } from "../api";
import { DuplicateWarningModal } from "./DuplicateWarningModal";
import { Link, Paperclip, Brain, Lightbulb, ListTree, Sparkles, Save, ChevronDown, ChevronUp, ChevronRight, Bot, Server, Flag } from "lucide-react";
import { CustomModelDropdown } from "./CustomModelDropdown";
@@ -233,13 +233,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
const shouldShowNodePicker = useMemo(() => hasMeaningfulNodeChoice(nodes), [nodes]);
const selectedNode = shouldShowNodePicker && nodeId ? nodes.find((node) => node.id === nodeId) : undefined;
const effectiveNodeId = shouldShowNodePicker && selectedNode ? selectedNode.id : undefined;
// AI Refinement state
const [isRefineMenuOpen, setIsRefineMenuOpen] = useState(false);
const [isRefining, setIsRefining] = useState(false);
const refineMenuRef = useRef<HTMLDivElement>(null);
const refineMenuPortalRef = useRef<HTMLDivElement>(null);
const [refineMenuPosition, setRefineMenuPosition] = useState<{ top: number; left: number } | null>(null);
useEffect(() => {
if (shouldShowNodePicker && (!nodeId || selectedNode)) {
return;
@@ -508,24 +501,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
if (!showDeps) setDepSearch("");
}, [showDeps]);
// Close refine menu when clicking outside
useEffect(() => {
if (!isRefineMenuOpen) return;
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as Node;
const clickedInsideTrigger = refineMenuRef.current?.contains(target);
const clickedInsidePortal = refineMenuPortalRef.current?.contains(target);
if (!clickedInsideTrigger && !clickedInsidePortal) {
setIsRefineMenuOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [isRefineMenuOpen]);
// Close model menu when clicking outside
useEffect(() => {
if (!isModelMenuOpen) return;
@@ -642,8 +617,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
setIsModelMenuOpen(false);
setModelMenuPosition(null);
setActiveModelSubmenu(null);
setIsRefineMenuOpen(false);
setIsRefining(false);
justResetRef.current = true;
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
@@ -919,10 +892,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
setShowDeps(false);
return;
}
if (isRefineMenuOpen) {
setIsRefineMenuOpen(false);
return;
}
if (showNodePicker) {
setShowNodePicker(false);
setNodePickerPosition(null);
@@ -970,7 +939,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
showNodePicker,
isModelMenuOpen,
activeModelSubmenu,
isRefineMenuOpen,
showPriorityPicker,
showWorkflowPicker,
projectId,
@@ -1081,44 +1049,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
});
}, [getEffectiveViewport]);
const updateRefineMenuPosition = useCallback(() => {
const trigger = refineMenuRef.current?.querySelector(".refine-button") as HTMLElement | null;
if (!trigger) return;
const rect = trigger.getBoundingClientRect();
const { width: viewportWidth, height: viewportHeight, offsetTop, offsetLeft } = getEffectiveViewport();
const horizontalPadding = 8;
const verticalPadding = 12;
const gap = 4;
const expectedMenuHeight = Math.min(200, Math.max(viewportHeight - verticalPadding * 2, 160));
const menuWidth = Math.min(200, viewportWidth - horizontalPadding * 2);
const triggerTop = rect.top - offsetTop;
const triggerBottom = rect.bottom - offsetTop;
const triggerLeft = rect.left - offsetLeft;
const spaceBelow = viewportHeight - triggerBottom;
const spaceAbove = triggerTop;
const openUpward = spaceBelow < expectedMenuHeight && spaceAbove > spaceBelow;
const left = Math.min(
Math.max(triggerLeft, horizontalPadding),
viewportWidth - horizontalPadding - menuWidth,
) + offsetLeft;
const top = openUpward
? Math.max(verticalPadding + offsetTop, triggerTop - expectedMenuHeight - gap + offsetTop)
: Math.min(
triggerBottom + gap + offsetTop,
viewportHeight + offsetTop - verticalPadding - expectedMenuHeight,
);
setRefineMenuPosition({
top,
left,
});
}, [getEffectiveViewport]);
const updateWorkflowPickerPosition = useCallback(() => {
const trigger = workflowTriggerRef.current;
if (!trigger) return;
@@ -1421,31 +1351,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
};
}, [isModelMenuOpen, updateModelMenuPosition]);
// Keep refine menu portal anchored during scroll/resize
useEffect(() => {
if (!isRefineMenuOpen) return;
const handleReposition = () => updateRefineMenuPosition();
window.addEventListener("resize", handleReposition);
window.addEventListener("scroll", handleReposition, true);
const vv = window.visualViewport;
if (vv) {
vv.addEventListener("resize", handleReposition);
vv.addEventListener("scroll", handleReposition);
}
return () => {
window.removeEventListener("resize", handleReposition);
window.removeEventListener("scroll", handleReposition, true);
if (vv) {
vv.removeEventListener("resize", handleReposition);
vv.removeEventListener("scroll", handleReposition);
}
};
}, [isRefineMenuOpen, updateRefineMenuPosition]);
// Keep workflow picker portal anchored during scroll/resize
useEffect(() => {
if (!showWorkflowPicker) return;
@@ -1659,29 +1564,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
handleSubmit();
}, [handleSubmit]);
const handleRefine = useCallback(async (type: RefinementType) => {
const trimmed = description.trim();
if (!trimmed || isRefining) return;
setIsRefineMenuOpen(false);
setIsRefining(true);
try {
const refined = await refineText(trimmed, type, projectId);
setDescription(refined);
addToast(t("tasks.descriptionRefined", "Description refined with AI"), "success");
// Auto-resize textarea after content update
if (textareaRef.current) {
textareaRef.current.style.height = "auto";
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`;
}
} catch (err) {
const errorMessage = getRefineErrorMessage(err);
addToast(errorMessage, "error");
} finally {
setIsRefining(false);
}
}, [description, isRefining, addToast, projectId]);
const truncate = (s: string, len: number) =>
s.length > len ? s.slice(0, len) + "…" : s;
@@ -2080,78 +1962,10 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
{t("tasks.subtask", "Subtask")}
</button>
)}
<div className="refine-trigger-wrap" ref={refineMenuRef}>
<button
type="button"
className={`btn btn-sm refine-button ${isRefining ? "refine-button--loading" : ""}`}
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
setIsRefineMenuOpen((prev) => {
const next = !prev;
if (next) {
// Compute position synchronously so the portal renders on first paint
updateRefineMenuPosition();
} else {
setRefineMenuPosition(null);
}
return next;
});
}}
disabled={!description.trim() || isRefining}
data-testid="refine-button"
title={t("tasks.refineButtonTitle", "Refine description with AI")}
>
<Sparkles size={12} style={{ verticalAlign: "middle" }} />
{isRefining ? t("tasks.refining", "Refining...") : t("tasks.refine", "Refine")}
</button>
{isRefineMenuOpen && portalRoot && refineMenuPosition && createPortal(
<div
ref={refineMenuPortalRef}
className="refine-menu refine-menu--portal"
onMouseDown={(e) => e.preventDefault()}
style={{
position: "fixed",
top: `${refineMenuPosition.top}px`,
left: `${refineMenuPosition.left}px`,
}}
>
<div
className="refine-menu-item"
onClick={() => handleRefine("clarify")}
data-testid="refine-clarify"
>
<div className="refine-menu-item-title">{t("tasks.refineClarify", "Clarify")}</div>
<div className="refine-menu-item-desc">{t("tasks.refineClarifyDesc", "Make the description clearer and more specific")}</div>
</div>
<div
className="refine-menu-item"
onClick={() => handleRefine("add-details")}
data-testid="refine-add-details"
>
<div className="refine-menu-item-title">{t("tasks.refineAddDetails", "Add details")}</div>
<div className="refine-menu-item-desc">{t("tasks.refineAddDetailsDesc", "Add implementation details and context")}</div>
</div>
<div
className="refine-menu-item"
onClick={() => handleRefine("expand")}
data-testid="refine-expand"
>
<div className="refine-menu-item-title">{t("tasks.refineExpand", "Expand")}</div>
<div className="refine-menu-item-desc">{t("tasks.refineExpandDesc", "Expand into a more comprehensive description")}</div>
</div>
<div
className="refine-menu-item"
onClick={() => handleRefine("simplify")}
data-testid="refine-simplify"
>
<div className="refine-menu-item-title">{t("tasks.refineSimplify", "Simplify")}</div>
<div className="refine-menu-item-desc">{t("tasks.refineSimplifyDesc", "Simplify and make more concise")}</div>
</div>
</div>,
portalRoot,
)}
</div>
{/*
FNXC:QuickAddRefine 2026-06-30-00:00:
Quick Add intentionally omits AI Refine so the compact create row has no refine button, menu, loading state, or /ai/refine-text path. New Task and TaskForm keep their dedicated refine affordance for richer task drafting.
*/}
<div className="dep-trigger-wrap">
<button
ref={depTriggerRef}

View File

@@ -186,7 +186,6 @@ vi.mock("../../api", () => ({
}),
fetchAuthStatus: vi.fn().mockResolvedValue({ providers: [] }),
refineText: vi.fn(),
getRefineErrorMessage: vi.fn((err) => err?.message || "Failed to refine text. Please try again."),
fetchAgents: vi.fn().mockResolvedValue([]),
checkDuplicateTasks: vi.fn().mockResolvedValue([]),
uploadAttachment: vi.fn().mockResolvedValue({}),
@@ -391,7 +390,6 @@ const QUICK_ENTRY_ACTION_BUTTONS = [
["GitHub", "quick-entry-github-toggle"],
["Priority", "quick-entry-priority-button"],
["Subtask", "subtask-button"],
["Refine", "refine-button"],
["Deps", "quick-entry-deps"],
["Models", "quick-entry-models"],
["Node", "quick-entry-node-button"],
@@ -538,7 +536,7 @@ describe("QuickEntryBox", () => {
fireEvent.change(textarea, {
target: {
value: "This is a long multiline task description that should use the available quick entry width before wrapping.\nIt should not reserve the description refine-button gutter.",
value: "This is a long multiline task description that should use the available quick entry width before wrapping.\nIt should not reserve the former description-refine gutter.",
},
});
@@ -1179,9 +1177,6 @@ describe("QuickEntryBox", () => {
case "quick-entry-attach":
expect(attachClickSpy).toHaveBeenCalled();
break;
case "refine-button":
expect(await screen.findByTestId("refine-clarify")).toBeTruthy();
break;
case "subtask-button":
expect(helpers.onSubtaskBreakdown).toHaveBeenCalledWith("Adjust options without keyboard");
break;
@@ -3621,240 +3616,73 @@ describe("QuickEntryBox", () => {
});
});
describe("AI Refine feature", () => {
it("shows refine button when expanded and text is entered", () => {
renderQuickEntryBox({});
describe("Quick Add refine removal", () => {
function expectQuickAddRefineAbsent() {
expect(screen.queryByTestId("refine-button")).not.toBeInTheDocument();
expect(screen.queryByTestId("refine-clarify")).not.toBeInTheDocument();
expect(screen.queryByTestId("refine-add-details")).not.toBeInTheDocument();
expect(screen.queryByTestId("refine-expand")).not.toBeInTheDocument();
expect(screen.queryByTestId("refine-simplify")).not.toBeInTheDocument();
expect(document.querySelector(".quick-entry-box .refine-trigger-wrap")).toBeNull();
expect(document.querySelector(".quick-entry-box .refine-menu")).toBeNull();
expect(screen.queryByTitle("Refine description with AI")).not.toBeInTheDocument();
}
// Controls region starts expanded/visible
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
// Type something
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Task to refine" } });
// Now the refine button should be visible
expect(screen.getByTestId("refine-button")).toBeTruthy();
});
it("refine button is hidden when textarea is empty", () => {
it("omits refine controls for empty and populated desktop quick-add states", () => {
renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
// Type something
fireEvent.change(textarea, { target: { value: "Some text" } });
expect(screen.getByTestId("refine-button")).toBeTruthy();
expectQuickAddRefineAbsent();
// Clear the input
fireEvent.change(textarea, { target: { value: "" } });
fireEvent.change(textarea, { target: { value: "Task text that used to enable refine" } });
// Button should be hidden/disabled (might be hidden when controls collapse)
const refineButton = screen.queryByTestId("refine-button");
if (refineButton) {
expect((refineButton as HTMLButtonElement).disabled).toBe(true);
}
expectQuickAddRefineAbsent();
expect(screen.queryByText("Refining...")).not.toBeInTheDocument();
});
it("opens refine menu on button click", () => {
it("omits refine controls in mobile while preserving adjacent actions", () => {
mockMobileViewport();
renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Mobile quick-add task" } });
fireEvent.change(textarea, { target: { value: "Task to refine" } });
fireEvent.click(screen.getByTestId("refine-button"));
// Menu should be visible with all options
expect(screen.getByTestId("refine-clarify")).toBeTruthy();
expect(screen.getByTestId("refine-add-details")).toBeTruthy();
expect(screen.getByTestId("refine-expand")).toBeTruthy();
expect(screen.getByTestId("refine-simplify")).toBeTruthy();
expectQuickAddRefineAbsent();
expect(screen.queryByRole("button", { name: /^Refine$/i })).not.toBeInTheDocument();
expect(screen.getByTestId("quick-entry-save")).toBeInTheDocument();
expect(screen.getByTestId("quick-entry-attach")).toBeInTheDocument();
expect(screen.getByTestId("subtask-button")).toBeInTheDocument();
expect(screen.getByTestId("quick-entry-deps")).toBeInTheDocument();
expect(screen.getByTestId("quick-entry-models")).toBeInTheDocument();
});
it("closes refine menu on Escape key", () => {
renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Task to refine" } });
fireEvent.click(screen.getByTestId("refine-button"));
// Menu should be open
expect(screen.getByTestId("refine-clarify")).toBeTruthy();
// Press Escape
fireEvent.keyDown(textarea, { key: "Escape" });
// Menu should be closed but input preserved
expect(screen.queryByTestId("refine-clarify")).toBeNull();
expect((textarea as HTMLTextAreaElement).value).toBe("Task to refine");
});
it("closes refine menu when option is selected", async () => {
const { refineText } = await import("../../api");
vi.mocked(refineText).mockResolvedValueOnce("Refined description");
renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Original text" } });
fireEvent.click(screen.getByTestId("refine-button"));
// Menu should be open
expect(screen.getByTestId("refine-clarify")).toBeTruthy();
// Click on an option
fireEvent.click(screen.getByTestId("refine-clarify"));
// Menu should close
await waitFor(() => {
expect(screen.queryByTestId("refine-clarify")).toBeNull();
});
});
it("closes refine menu immediately when option is clicked (before API response)", async () => {
const { refineText } = await import("../../api");
// Use a slow promise to ensure we can check the menu is closed before it resolves
vi.mocked(refineText).mockImplementation(
() => new Promise((resolve) => setTimeout(() => resolve("Refined"), 500)),
);
renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Original text" } });
fireEvent.click(screen.getByTestId("refine-button"));
// Menu should be open
expect(screen.getByTestId("refine-clarify")).toBeTruthy();
// Click on an option
fireEvent.click(screen.getByTestId("refine-clarify"));
// Menu should close IMMEDIATELY (before the slow promise resolves)
expect(screen.queryByTestId("refine-clarify")).toBeNull();
// The loading state should still be shown
const refineButton = screen.getByTestId("refine-button");
expect(refineButton.textContent).toContain("Refining...");
});
it("successful refinement updates textarea content", async () => {
const { refineText } = await import("../../api");
vi.mocked(refineText).mockResolvedValueOnce("Refined description");
it("does not call the AI refine API path from Quick Add", async () => {
const api = await import("../../api");
const refineSpy = vi.mocked(api.refineText);
const { props } = renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Original text" } });
fireEvent.click(screen.getByTestId("refine-button"));
fireEvent.click(screen.getByTestId("refine-clarify"));
await waitFor(() => {
expect(refineText).toHaveBeenCalledWith("Original text", "clarify", TEST_PROJECT_ID);
});
// Textarea should be updated
await waitFor(() => {
expect((textarea as HTMLTextAreaElement).value).toBe("Refined description");
});
// Success toast should be shown
await waitFor(() => {
expect(props.addToast).toHaveBeenCalledWith("Description refined with AI", "success");
});
});
it("failed refinement shows toast and preserves original text", async () => {
const { refineText } = await import("../../api");
vi.mocked(refineText).mockRejectedValueOnce(new Error("Rate limit exceeded"));
const { getRefineErrorMessage } = await import("../../api");
const { props } = renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Original text" } });
fireEvent.click(screen.getByTestId("refine-button"));
fireEvent.click(screen.getByTestId("refine-clarify"));
await waitFor(() => {
expect(props.addToast).toHaveBeenCalled();
});
// Original text should be preserved
expect((textarea as HTMLTextAreaElement).value).toBe("Original text");
});
it("loading state disables button during refinement", async () => {
const { refineText } = await import("../../api");
// Slow down the promise to see loading state
vi.mocked(refineText).mockImplementation(
() => new Promise((resolve) => setTimeout(() => resolve("Refined text"), 100)),
);
renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Original text" } });
fireEvent.click(screen.getByTestId("refine-button"));
fireEvent.click(screen.getByTestId("refine-clarify"));
// Button should show loading text
await waitFor(() => {
expect(screen.getByText("Refining...")).toBeTruthy();
});
// Button should be disabled
const refineButton = screen.getByTestId("refine-button");
expect((refineButton as HTMLButtonElement).disabled).toBe(true);
});
it("auto-resizes textarea after refinement", async () => {
const { refineText } = await import("../../api");
vi.mocked(refineText).mockResolvedValueOnce("Refined description with much more content here");
renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Short" } });
fireEvent.click(screen.getByTestId("refine-button"));
fireEvent.click(screen.getByTestId("refine-expand"));
await waitFor(() => {
expect(refineText).toHaveBeenCalled();
});
});
it("resets refine state when form is reset after creation", async () => {
const { refineText } = await import("../../api");
vi.mocked(refineText).mockResolvedValueOnce("Refined text");
const { props } = renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
// Open refine menu but don't select anything
fireEvent.change(textarea, { target: { value: "Task" } });
fireEvent.click(screen.getByTestId("refine-button"));
expect(screen.getByTestId("refine-clarify")).toBeTruthy();
// Submit the form
fireEvent.keyDown(textarea, { key: "Enter" });
fireEvent.change(textarea, { target: { value: "Create this without refinement" } });
fireEvent.click(screen.getByTestId("quick-entry-save"));
await waitFor(() => {
expect(props.onCreate).toHaveBeenCalled();
});
expect(refineSpy).not.toHaveBeenCalled();
});
// After reset, refine menu should be closed
expect(screen.queryByTestId("refine-clarify")).toBeNull();
it("keeps the action row free of orphaned refine shells when Subtask is omitted", () => {
renderQuickEntryBox({ onSubtaskBreakdown: undefined });
expandQuickEntry();
const actionsContainer = screen.getByTestId("quick-entry-actions");
expectQuickAddRefineAbsent();
expect(screen.queryByTestId("subtask-button")).not.toBeInTheDocument();
expect(actionsContainer.querySelector(".refine-trigger-wrap")).toBeNull();
expect(actionsContainer.contains(screen.getByTestId("quick-entry-deps"))).toBe(true);
expect(actionsContainer.contains(screen.getByTestId("quick-entry-models"))).toBe(true);
});
});
@@ -3961,7 +3789,7 @@ describe("QuickEntryBox", () => {
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(screen.getByTestId("subtask-button")).toBeTruthy();
expect(screen.getByTestId("refine-button")).toBeTruthy();
expect(screen.queryByTestId("refine-button")).not.toBeInTheDocument();
expect(screen.getByTestId("quick-entry-deps")).toBeTruthy();
expect(screen.getByTestId("quick-entry-models")).toBeTruthy();
expect(screen.getByTestId("quick-entry-save")).toBeTruthy();
@@ -3980,7 +3808,7 @@ describe("QuickEntryBox", () => {
});
describe("Consolidated actions layout (FN-781, FN-1088)", () => {
it("renders Subtask and Refine in actions area without a Plan shell", () => {
it("renders Subtask in actions area without Plan or Refine shells", () => {
renderQuickEntryBox({});
expandQuickEntry();
@@ -3988,8 +3816,9 @@ describe("QuickEntryBox", () => {
const actionsContainer = screen.getByTestId("quick-entry-actions");
expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(screen.queryByTestId("refine-button")).not.toBeInTheDocument();
expect(actionsContainer.querySelector(".refine-trigger-wrap")).toBeNull();
expect(actionsContainer.contains(screen.getByTestId("subtask-button"))).toBe(true);
expect(actionsContainer.contains(screen.getByTestId("refine-button"))).toBe(true);
});
it("hides the Subtask quick-add action without leaving an action-row shell when the callback is omitted", () => {
@@ -4000,7 +3829,8 @@ describe("QuickEntryBox", () => {
expect(screen.queryByTestId("subtask-button")).not.toBeInTheDocument();
expect(screen.queryByTitle("Break down into AI-generated subtasks")).not.toBeInTheDocument();
expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(actionsContainer.contains(screen.getByTestId("refine-button"))).toBe(true);
expect(screen.queryByTestId("refine-button")).not.toBeInTheDocument();
expect(actionsContainer.querySelector(".refine-trigger-wrap")).toBeNull();
});
it("does not render actions when not expanded", () => {
@@ -4031,14 +3861,15 @@ describe("QuickEntryBox", () => {
expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(screen.getByTestId("subtask-button")).toBeTruthy();
expect(screen.getByTestId("refine-button")).toBeTruthy();
expect(screen.queryByTestId("refine-button")).not.toBeInTheDocument();
expect(screen.getByTestId("quick-entry-deps")).toBeTruthy();
expect(screen.getByTestId("quick-entry-models")).toBeTruthy();
expect(screen.getByTestId("quick-entry-save")).toBeTruthy();
expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(screen.queryByTestId("refine-button")).not.toBeInTheDocument();
expect(controlsPanel?.querySelector(".refine-trigger-wrap")).toBeNull();
expect(controlsPanel?.contains(screen.getByTestId("subtask-button"))).toBe(true);
expect(controlsPanel?.contains(screen.getByTestId("refine-button"))).toBe(true);
expect(controlsPanel?.contains(screen.getByTestId("quick-entry-deps"))).toBe(true);
expect(controlsPanel?.contains(screen.getByTestId("quick-entry-models"))).toBe(true);
expect(controlsPanel?.contains(screen.getByTestId("quick-entry-save"))).toBe(true);
@@ -4386,15 +4217,15 @@ describe("QuickEntryBox", () => {
expect(modelsButton.className).toContain("btn");
});
it("keeps Subtask and Refine buttons in touch-target classes without Plan", () => {
it("keeps Subtask touch-target classes without Plan or Refine", () => {
vi.spyOn(window, "innerWidth", "get").mockReturnValue(375);
renderQuickEntryBox({});
expandQuickEntry();
expect(screen.queryByRole("button", { name: /^Plan$/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /^Refine$/i })).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: /Subtask/i }).className).toContain("btn");
expect(screen.getByRole("button", { name: /Refine/i }).className).toContain("btn");
});
it("keeps model menu portal within mobile viewport", () => {
@@ -4429,7 +4260,7 @@ describe("QuickEntryBox", () => {
expect(rect.right).toBeLessThanOrEqual(viewportWidth);
});
it("responds to clicks for toggle, subtask, refine, and deps without Plan", async () => {
it("responds to clicks for toggle, subtask, and deps without Plan or Refine", async () => {
const onPlanningMode = vi.fn();
const onSubtaskBreakdown = vi.fn();
@@ -4456,12 +4287,9 @@ describe("QuickEntryBox", () => {
expect(onSubtaskBreakdown).toHaveBeenCalledWith("Break this down");
ensureExpanded();
fireEvent.change(input, { target: { value: "Refine this" } });
fireEvent.click(screen.getByRole("button", { name: /Refine/i }));
await waitFor(() => {
expect(screen.getByTestId("refine-clarify")).toBeInTheDocument();
});
fireEvent.change(input, { target: { value: "Refine should be absent" } });
expect(screen.queryByRole("button", { name: /^Refine$/i })).not.toBeInTheDocument();
expect(screen.queryByTestId("refine-clarify")).not.toBeInTheDocument();
ensureExpanded();
fireEvent.click(screen.getByTestId("quick-entry-deps"));