FN-7288: remove quick-add Plan buttons

Remove quick-add planning buttons from board create surfaces while preserving other planning entry points.

- Remove the Plan button, icon imports, handlers, test id, disabled state, tooltip, and click targets from QuickEntryBox and InlineCreateCard.
- Update quick-add and mobile board tests to assert the Plan affordance is absent while task creation and Subtask behavior remain available.
- Refresh dashboard docs and add a patch changeset describing the quick-add Plan removal.

Files changed:
 .changeset/fn-7288-remove-quick-add-plan-button.md |   7 +
 docs/dashboard-guide.md                            |   4 +-
 docs/getting-started.md                            |   2 +-
 docs/workflow-steps.md                             |   2 +-
 .../dashboard/app/components/InlineCreateCard.tsx  |  38 +---
 .../dashboard/app/components/QuickEntryBox.tsx     |  38 +---
 .../components/__tests__/InlineCreateCard.test.tsx |  56 +++---
 .../components/__tests__/QuickEntryBox.test.tsx    | 196 +++++----------------
 .../app/components/__tests__/board-mobile.test.tsx |   6 +-
 9 files changed, 97 insertions(+), 252 deletions(-)

Fusion-Task-Id: FN-7288

Fusion-Task-Lineage: b4a5d6b6-83e7-4b0c-a38d-6ff36d8cd97e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-30 10:40:49 -07:00
parent f0a15db85c
commit 3cd5695f52
9 changed files with 97 additions and 252 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Remove the board quick-add Plan button while keeping New Task planning available.
category: fix
dev: QuickEntryBox and InlineCreateCard no longer render data-testid="plan-button" or Plan click targets.

View File

@@ -346,7 +346,7 @@ These values are sent with the Planning Mode create-task request as `branchSelec
<!-- FNXC:WorkflowCreateForwarding 2026-06-30-09:12: Dashboard create flows must forward the active real workflow id so planning/quick-create tasks do not briefly land on the default workflow or persist the synthetic All workflows aggregate. --> <!-- FNXC:WorkflowCreateForwarding 2026-06-30-09:12: Dashboard create flows must forward the active real workflow id so planning/quick-create tasks do not briefly land on the default workflow or persist the synthetic All workflows aggregate. -->
When inline quick-create, Planning Mode, or Subtask Breakdown is opened from a workflow-filtered board/list lane, the create request also carries that active workflow selection. Quick-created tasks appear on the selected workflow lane immediately while board-workflows metadata refreshes, and planning saves, planning breakdown saves, and subtask-breakdown saves create their tasks directly on the selected workflow lane instead of briefly landing on the default board. When Board is showing **All workflows**, quick-create uses the real workflow intake/default column that owns the affordance; it never submits the synthetic aggregate as a workflow id. When quick-create task creation, Planning Mode, or Subtask Breakdown runs from a workflow-filtered board/list lane, the create request also carries that active workflow selection. Quick-created tasks appear on the selected workflow lane immediately while board-workflows metadata refreshes, and planning saves, planning breakdown saves, and subtask-breakdown saves create their tasks directly on the selected workflow lane instead of briefly landing on the default board. When Board is showing **All workflows**, quick-create uses the real workflow intake/default column that owns the affordance; it never submits the synthetic aggregate as a workflow id.
The **New Task** dialog's workflow selector also defaults to the current or last selected Board/List workflow lane for the current project. If no valid lane has been selected, or the remembered lane was deleted, the selector falls back to the project default workflow and task creation omits an explicit `workflowId`. The **New Task** dialog's workflow selector also defaults to the current or last selected Board/List workflow lane for the current project. If no valid lane has been selected, or the remembered lane was deleted, the selector falls back to the project default workflow and task creation omits an explicit `workflowId`.
@@ -373,7 +373,7 @@ Rules:
- `Merge target / base branch` stays optional for all modes and uses the same branch-dropdown + `Custom…` fallback behavior as Planning Mode. - `Merge target / base branch` stays optional for all modes and uses the same branch-dropdown + `Custom…` fallback behavior as Planning Mode.
- In **More options → Model Configuration**, **Auto-merge** is a per-task override with three states: **Default** (follow project setting), **Enabled**, or **Disabled**. - In **More options → Model Configuration**, **Auto-merge** is a per-task override with three states: **Default** (follow project setting), **Enabled**, or **Disabled**.
The dialog also exposes the board quick-add AI handoffs: **Plan** opens Planning Mode with the current description, and **Subtask** opens Subtask Breakdown with the current description when **Settings → Experimental Features → Subtask Breakdown** is enabled. The Subtask handoff is hidden by default; visible handoff buttons remain disabled until the description has content, matching the quick-add row behavior. **Execution mode** and optional workflow-step selection are available in the New Task dialog as well as quick entry, so users can choose Fast or standard execution and opt into workflow-specific creation-time steps before creating a task from either surface. The dialog also exposes AI handoffs that quick-add no longer shows: **Plan** opens Planning Mode with the current description, and **Subtask** opens Subtask Breakdown with the current description when **Settings → Experimental Features → Subtask Breakdown** is enabled. The Subtask handoff is hidden by default; visible handoff buttons remain disabled until the description has content, matching the quick-add row behavior for Subtask. **Execution mode** and optional workflow-step selection are available in the New Task dialog as well as quick entry, so users can choose Fast or standard execution and opt into workflow-specific creation-time steps before creating a task from either surface.
The full **New Task** dialog includes a compact **GitHub issue or PR** picker near the description. It detects GitHub remotes for the current project, auto-selects a single remote or `origin`, and asks you to choose a remote when multiple non-`origin` remotes are available. Selecting an issue replaces the description with a prompt that tells the executor to fetch/read the issue and includes `Source: <issue-url>`; selecting a pull request creates a PR-focused prompt with `PR: <pr-url>` and explicit instructions to inspect the PR conversation, review comments, checks, and changed files, then resolve or address actionable review comments. If you already typed a description, Fusion asks before replacing it. This picker only seeds the prompt; it does not import, close, or comment on GitHub items. On mobile, the full-screen New Task sheet keeps the GitHub picker, dependency picker, agent picker, quick handoff buttons, and action row tappable and scrollable even when the keyboard reduces the visual viewport. The full **New Task** dialog includes a compact **GitHub issue or PR** picker near the description. It detects GitHub remotes for the current project, auto-selects a single remote or `origin`, and asks you to choose a remote when multiple non-`origin` remotes are available. Selecting an issue replaces the description with a prompt that tells the executor to fetch/read the issue and includes `Source: <issue-url>`; selecting a pull request creates a PR-focused prompt with `PR: <pr-url>` and explicit instructions to inspect the PR conversation, review comments, checks, and changed files, then resolve or address actionable review comments. If you already typed a description, Fusion asks before replacing it. This picker only seeds the prompt; it does not import, close, or comment on GitHub items. On mobile, the full-screen New Task sheet keeps the GitHub picker, dependency picker, agent picker, quick handoff buttons, and action row tappable and scrollable even when the keyboard reduces the visual viewport.

View File

@@ -114,7 +114,7 @@ Create tasks from the board or CLI.
### Option B: Planning ### Option B: Planning
Open **Planning** from the left sidebar on desktop/tablet, or use the board/New Task **Plan** action to send your draft into AI planning mode: Open **Planning** from the left sidebar on desktop/tablet, or use the **New Task** dialog's **Plan** action to send your draft into AI planning mode:
- Fusion asks clarifying questions - Fusion asks clarifying questions
- Produces a structured summary - Produces a structured summary

View File

@@ -81,7 +81,7 @@ Use this inventory as the documentation map for current workflow behavior:
| Agent workflow tools | Agents can list/get/create/update/delete workflows, inspect traits, read/write workflow settings, select workflows for explicit task contexts, and pass `workflow_id` when creating/delegating tasks. Prompt-injectable lanes strip approval-bypass flags on workflow writes. | [Agents](./agents.md#interactive-cli-chat) and [CLI Reference](./cli-reference.md#published-agent-extension-workflow-tools). | | Agent workflow tools | Agents can list/get/create/update/delete workflows, inspect traits, read/write workflow settings, select workflows for explicit task contexts, and pass `workflow_id` when creating/delegating tasks. Prompt-injectable lanes strip approval-bypass flags on workflow writes. | [Agents](./agents.md#interactive-cli-chat) and [CLI Reference](./cli-reference.md#published-agent-extension-workflow-tools). |
| Routing boundary | Agents may select/change a workflow only for explicit user requests or tasks they created; no-commit markers do not imply Quick fix or any other workflow. | This page, [Selecting workflows](#selecting-workflows); [Agents](./agents.md#interactive-cli-chat). | | Routing boundary | Agents may select/change a workflow only for explicit user requests or tasks they created; no-commit markers do not imply Quick fix or any other workflow. | This page, [Selecting workflows](#selecting-workflows); [Agents](./agents.md#interactive-cli-chat). |
| Dashboard board/list/graph selection | Board/List/Header/Graph share durable per-project workflow selection; stale saved ids fall back to a valid workflow. Board adds a dashboard-only **All workflows** aggregate and task workflow-name badges; Graph uses **All workflows** for the full active graph. | [Dashboard Guide → Board View](./dashboard-guide.md#board-view), [Graph View](./dashboard-guide.md#graph-view), and [Workflow Selection and Editor](./dashboard-guide.md#workflow-selection-and-editor). | | Dashboard board/list/graph selection | Board/List/Header/Graph share durable per-project workflow selection; stale saved ids fall back to a valid workflow. Board adds a dashboard-only **All workflows** aggregate and task workflow-name badges; Graph uses **All workflows** for the full active graph. | [Dashboard Guide → Board View](./dashboard-guide.md#board-view), [Graph View](./dashboard-guide.md#graph-view), and [Workflow Selection and Editor](./dashboard-guide.md#workflow-selection-and-editor). |
| Create/planning forwarding | Inline quick-create, Planning Mode, Subtask Breakdown, and the New Task dialog forward the active real workflow id when creating tasks; **All workflows** quick-create chooses a real workflow intake/default column instead of saving a synthetic aggregate id. | [Dashboard Guide → Planning Mode](./dashboard-guide.md#planning-mode). | | Create/planning forwarding | Quick-create task creation, Planning Mode, Subtask Breakdown, and the New Task dialog forward the active real workflow id when creating tasks; **All workflows** quick-create chooses a real workflow intake/default column instead of saving a synthetic aggregate id. | [Dashboard Guide → Planning Mode](./dashboard-guide.md#planning-mode). |
### Skill-backed workflow steps ### Skill-backed workflow steps

View File

@@ -2,7 +2,7 @@ import "./InlineCreateCard.css";
import { useState, useCallback, useEffect, useRef } from "react"; import { useState, useCallback, useEffect, useRef } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Brain, Link, Lightbulb, ListTree, Zap, ChevronDown, ChevronUp, Bot, Maximize2, Minimize2, Server } from "lucide-react"; import { Brain, Link, ListTree, Zap, ChevronDown, ChevronUp, Bot, Maximize2, Minimize2, Server } from "lucide-react";
import { DEFAULT_TASK_PRIORITY, TASK_PRIORITIES, type Task, type TaskPriority, type Settings, type ResolvedWorkflowOptionalStep } from "@fusion/core"; import { DEFAULT_TASK_PRIORITY, TASK_PRIORITIES, type Task, type TaskPriority, type Settings, type ResolvedWorkflowOptionalStep } from "@fusion/core";
import { getErrorMessage } from "@fusion/core"; import { getErrorMessage } from "@fusion/core";
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";
@@ -39,7 +39,7 @@ interface InlineCreateCardProps {
*/ */
availableModels?: ModelInfo[]; availableModels?: ModelInfo[];
/** /**
* Called when the user clicks the "Plan" button to open planning mode. * Preserved for shared create-surface prop compatibility. Inline quick-create intentionally omits Plan.
*/ */
onPlanningMode?: (initialPlan: string, workflowId?: string | null) => void; onPlanningMode?: (initialPlan: string, workflowId?: string | null) => void;
/** /**
@@ -82,7 +82,6 @@ export function InlineCreateCard({
addToast, addToast,
projectId, projectId,
availableModels, availableModels,
onPlanningMode,
onSubtaskBreakdown, onSubtaskBreakdown,
}: InlineCreateCardProps) { }: InlineCreateCardProps) {
const { t } = useTranslation("app"); const { t } = useTranslation("app");
@@ -693,23 +692,10 @@ export function InlineCreateCard({
e.preventDefault(); e.preventDefault();
}, []); }, []);
const handlePlanClick = useCallback(() => { /*
const trimmed = description.trim(); FNXC:InlineCreate 2026-06-30-00:00:
if (!trimmed) { Inline quick-create intentionally omits the Plan button, icon, disabled state, tooltip, and click target while preserving Subtask and task creation controls.
addToast(t("inline.enterDescriptionFirst", "Enter a description first"), "error"); */
return;
}
if (selectedWorkflowId !== null) {
onPlanningMode?.(trimmed, selectedWorkflowId);
} else {
onPlanningMode?.(trimmed);
}
/*
FNXC:QuickAddPlanningPreserve 2026-06-22-00:00:
Opening planning mode must keep the inline-create description and scoped draft available when the user exits without creating tasks. Planning completion owns the eventual draft clear.
*/
}, [description, onPlanningMode, selectedWorkflowId, addToast, t]);
const handleSubtaskClick = useCallback(() => { const handleSubtaskClick = useCallback(() => {
const trimmed = description.trim(); const trimmed = description.trim();
if (!trimmed) { if (!trimmed) {
@@ -881,18 +867,6 @@ export function InlineCreateCard({
{isExpanded && ( {isExpanded && (
<div id="inline-create-controls" className="inline-create-footer"> <div id="inline-create-controls" className="inline-create-footer">
<div className="inline-create-controls"> <div className="inline-create-controls">
<button
type="button"
className="btn btn-sm"
onClick={handlePlanClick}
onMouseDown={(e) => e.preventDefault()}
disabled={!description.trim()}
data-testid="plan-button"
title={t("inline.openPlanningMode", "Open planning mode with current description")}
>
<Lightbulb size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
{t("inline.plan", "Plan")}
</button>
{/* FNXC:QuickAddSubtaskFlag 2026-06-21-00:00: Render no Subtask button or orphaned inline-create click target unless the default-off `subtaskBreakdown` experiment wires this callback. */} {/* FNXC:QuickAddSubtaskFlag 2026-06-21-00:00: Render no Subtask button or orphaned inline-create click target unless the default-off `subtaskBreakdown` experiment wires this callback. */}
{onSubtaskBreakdown && ( {onSubtaskBreakdown && (
<button <button

View File

@@ -31,7 +31,8 @@ interface QuickEntryBoxProps {
tasks?: Task[]; tasks?: Task[];
availableModels?: ModelInfo[]; availableModels?: ModelInfo[];
/** /**
* Called when the user clicks the "Plan" button to open planning mode. * Preserved for callers that still pass planning handoff props through shared quick-create plumbing.
* QuickEntryBox intentionally does not render a quick-add Plan button.
*/ */
onPlanningMode?: (initialPlan: string, workflowId?: string | null) => void; onPlanningMode?: (initialPlan: string, workflowId?: string | null) => void;
/** /**
@@ -123,7 +124,7 @@ function resolveQuickAddWorkflowId(
return workflowOptions[0]?.id ?? null; return workflowOptions[0]?.id ?? null;
} }
export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, onPlanningMode, onSubtaskBreakdown, workflowId, workflowOptions, defaultWorkflowId, projectId, autoExpand = true, defaultExpanded = true, singleLine = false, favoriteProviders: parentFavoriteProviders, favoriteModels: parentFavoriteModels, onToggleFavorite: parentToggleFavorite, onToggleModelFavorite: parentToggleModelFavorite, onOpenTask }: QuickEntryBoxProps) { export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, onSubtaskBreakdown, workflowId, workflowOptions, defaultWorkflowId, projectId, autoExpand = true, defaultExpanded = true, singleLine = false, favoriteProviders: parentFavoriteProviders, favoriteModels: parentFavoriteModels, onToggleFavorite: parentToggleFavorite, onToggleModelFavorite: parentToggleModelFavorite, onOpenTask }: QuickEntryBoxProps) {
const { t } = useTranslation("app"); const { t } = useTranslation("app");
const [description, setDescription] = useState(() => { const [description, setDescription] = useState(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
@@ -1477,23 +1478,10 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
} }
}, [favoriteModels, favoriteProviders, parentToggleModelFavorite]); }, [favoriteModels, favoriteProviders, parentToggleModelFavorite]);
const handlePlanClick = useCallback(() => { /*
const trimmed = description.trim(); FNXC:QuickEntry 2026-06-30-00:00:
if (!trimmed) { Quick-add intentionally exposes no Plan button, disabled Plan state, tooltip, test id, or click target. Keep non-quick-add planning entry points such as the New Task dialog and model-menu planning lane intact.
addToast(t("tasks.enterDescriptionFirst", "Enter a description first"), "error"); */
return;
}
if (selectedWorkflowForCreate !== undefined) {
onPlanningMode?.(trimmed, selectedWorkflowForCreate);
} else {
onPlanningMode?.(trimmed);
}
/*
FNXC:QuickAddPlanningPreserve 2026-06-22-00:00:
Opening planning mode must preserve the quick-add description and scoped draft so exiting planning without creating tasks restores the user's text. The draft is cleared only by planning-completion handlers.
*/
}, [description, onPlanningMode, selectedWorkflowForCreate, addToast, t]);
const handleSubtaskClick = useCallback(() => { const handleSubtaskClick = useCallback(() => {
const trimmed = description.trim(); const trimmed = description.trim();
if (!trimmed) { if (!trimmed) {
@@ -1857,18 +1845,6 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
triggerTestId="quick-entry-optional-steps-trigger" triggerTestId="quick-entry-optional-steps-trigger"
/> />
{/* FNXC:QuickEntry 2026-06-30-00:00: Quick Add Plan stays a normal text action without a Lightbulb icon so the action row preserves behavior while removing the extra visual affordance and icon spacing shell. */}
<button
type="button"
className="btn btn-sm"
onClick={handlePlanClick}
onMouseDown={(e) => e.preventDefault()}
disabled={!description.trim()}
data-testid="plan-button"
title={t("tasks.planButtonTitle", "Open planning mode with current description")}
>
{t("tasks.plan", "Plan")}
</button>
{/* FNXC:QuickAddSubtaskFlag 2026-06-21-00:00: Render no Subtask button or click target unless App wires the default-off `subtaskBreakdown` experiment callback. */} {/* FNXC:QuickAddSubtaskFlag 2026-06-21-00:00: Render no Subtask button or click target unless App wires the default-off `subtaskBreakdown` experiment callback. */}
{onSubtaskBreakdown && ( {onSubtaskBreakdown && (
<button <button

View File

@@ -16,7 +16,6 @@ vi.mock("lucide-react", () => ({
Search: () => null, Search: () => null,
Sparkles: () => null, Sparkles: () => null,
Terminal: () => null, Terminal: () => null,
Lightbulb: () => null,
ListTree: () => null, ListTree: () => null,
Zap: () => null, Zap: () => null,
ChevronDown: () => null, ChevronDown: () => null,
@@ -812,38 +811,38 @@ describe("InlineCreateCard dependency dropdown search", () => {
}); });
}); });
describe("InlineCreateCard Plan and Subtask buttons", () => { describe("InlineCreateCard Subtask controls without Plan", () => {
it("renders Plan and Subtask buttons disabled when description is empty", () => { it("omits Plan and renders Subtask disabled when description is empty", () => {
renderCard(); renderCard();
expandCard(); expandCard();
const planButton = screen.getByTestId("plan-button") as HTMLButtonElement;
const subtaskButton = screen.getByTestId("subtask-button") as HTMLButtonElement; const subtaskButton = screen.getByTestId("subtask-button") as HTMLButtonElement;
expect(planButton.disabled).toBe(true); expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /^Plan$/i })).not.toBeInTheDocument();
expect(subtaskButton.disabled).toBe(true); expect(subtaskButton.disabled).toBe(true);
}); });
it("enables Plan and Subtask buttons when description is entered", () => { it("omits Plan and enables Subtask when description is entered", () => {
renderCard(); renderCard();
expandCard(); expandCard();
const textarea = screen.getByPlaceholderText("What needs to be done?"); const textarea = screen.getByPlaceholderText("What needs to be done?");
fireEvent.change(textarea, { target: { value: "Test task" } }); fireEvent.change(textarea, { target: { value: "Test task" } });
const planButton = screen.getByTestId("plan-button") as HTMLButtonElement;
const subtaskButton = screen.getByTestId("subtask-button") as HTMLButtonElement; const subtaskButton = screen.getByTestId("subtask-button") as HTMLButtonElement;
expect(planButton.disabled).toBe(false); expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(subtaskButton.disabled).toBe(false); expect(subtaskButton.disabled).toBe(false);
}); });
it("calls onPlanningMode with description and preserves input draft when Plan clicked", () => { it("omits the Plan handoff while preserving input draft", () => {
const onPlanningMode = vi.fn(); const onPlanningMode = vi.fn();
renderCard([], { onPlanningMode }); renderCard([], { onPlanningMode });
expandCard(); expandCard();
const textarea = screen.getByPlaceholderText("What needs to be done?") as HTMLTextAreaElement; const textarea = screen.getByPlaceholderText("What needs to be done?") as HTMLTextAreaElement;
fireEvent.change(textarea, { target: { value: " Plan this task " } }); fireEvent.change(textarea, { target: { value: " Plan this task " } });
fireEvent.click(screen.getByTestId("plan-button"));
expect(onPlanningMode).toHaveBeenCalledWith("Plan this task"); expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(screen.queryByTitle("Open planning mode with current description")).not.toBeInTheDocument();
expect(onPlanningMode).not.toHaveBeenCalled();
expect(textarea.value).toBe(" Plan this task "); expect(textarea.value).toBe(" Plan this task ");
expect(localStorage.getItem(INLINE_CREATE_STORAGE_KEY)).toBe(" Plan this task "); expect(localStorage.getItem(INLINE_CREATE_STORAGE_KEY)).toBe(" Plan this task ");
}); });
@@ -869,22 +868,20 @@ describe("InlineCreateCard Plan and Subtask buttons", () => {
expect(controlsRow).toBeTruthy(); expect(controlsRow).toBeTruthy();
expect(screen.queryByTestId("subtask-button")).not.toBeInTheDocument(); expect(screen.queryByTestId("subtask-button")).not.toBeInTheDocument();
expect(screen.queryByTitle("Break down into AI-generated subtasks")).not.toBeInTheDocument(); expect(screen.queryByTitle("Break down into AI-generated subtasks")).not.toBeInTheDocument();
expect(controlsRow.contains(screen.getByTestId("plan-button"))).toBe(true); expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(controlsRow.querySelector(".dep-trigger")).toBeTruthy(); expect(controlsRow.querySelector(".dep-trigger")).toBeTruthy();
}); });
it("shows toast when Plan clicked with empty description (via direct handler call)", () => { it("does not leave a disabled Plan button for empty descriptions", () => {
const addToast = vi.fn(); const addToast = vi.fn();
const onPlanningMode = vi.fn(); const onPlanningMode = vi.fn();
renderCard([], { addToast, onPlanningMode }); renderCard([], { addToast, onPlanningMode });
expandCard(); expandCard();
// When no description, button is disabled - verify that behavior expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
const planButton = screen.getByTestId("plan-button") as HTMLButtonElement; expect(screen.queryByTitle("Open planning mode with current description")).not.toBeInTheDocument();
expect(planButton.disabled).toBe(true); expect(addToast).not.toHaveBeenCalled();
expect(onPlanningMode).not.toHaveBeenCalled();
// The handler validation exists but can't be triggered via click when disabled
// The disabled state is the primary UX protection
}); });
it("shows toast when Subtask clicked with empty description (via direct handler call)", () => { it("shows toast when Subtask clicked with empty description (via direct handler call)", () => {
@@ -1018,8 +1015,8 @@ describe("InlineCreateCard button visibility when collapsed", () => {
// Click toggle to expand // Click toggle to expand
expandCard(); expandCard();
// Now buttons should be visible // Now non-Plan buttons should be visible
expect(screen.getByTestId("plan-button")).toBeTruthy(); expect(screen.queryByTestId("plan-button")).toBeNull();
expect(screen.getByTestId("subtask-button")).toBeTruthy(); expect(screen.getByTestId("subtask-button")).toBeTruthy();
expect(screen.getByText(/Deps/)).toBeTruthy(); expect(screen.getByText(/Deps/)).toBeTruthy();
expect(screen.getByTestId("save-button")).toBeTruthy(); expect(screen.getByTestId("save-button")).toBeTruthy();
@@ -1030,7 +1027,8 @@ describe("InlineCreateCard button visibility when collapsed", () => {
// Expand // Expand
expandCard(); expandCard();
expect(screen.getByTestId("plan-button")).toBeTruthy(); expect(screen.queryByTestId("plan-button")).toBeNull();
expect(screen.getByTestId("subtask-button")).toBeTruthy();
// Collapse // Collapse
expandCard(); expandCard();
@@ -1173,7 +1171,7 @@ describe("InlineCreateCard button visibility when collapsed", () => {
}); });
describe("Consolidated controls layout (FN-781, FN-1292)", () => { describe("Consolidated controls layout (FN-781, FN-1292)", () => {
it("renders Plan, Subtask, Deps, Agent, and Models together in footer controls when expanded", () => { it("renders Subtask, Deps, Agent, and Models together in footer controls without Plan", () => {
renderCard(); renderCard();
expandCard(); expandCard();
@@ -1181,8 +1179,8 @@ describe("InlineCreateCard button visibility when collapsed", () => {
const controlsRow = document.querySelector(".inline-create-controls"); const controlsRow = document.querySelector(".inline-create-controls");
expect(controlsRow).toBeTruthy(); expect(controlsRow).toBeTruthy();
// Plan, Subtask, Deps, Agent, Browser Verify, Priority, Preset, Models all in one row // Subtask, Deps, Agent, Browser Verify, Priority, Preset, Models all in one row; Plan is intentionally omitted.
expect(controlsRow!.contains(screen.getByTestId("plan-button"))).toBe(true); expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(controlsRow!.contains(screen.getByTestId("subtask-button"))).toBe(true); expect(controlsRow!.contains(screen.getByTestId("subtask-button"))).toBe(true);
expect(controlsRow!.contains(screen.getByTestId("inline-create-agent-button"))).toBe(true); expect(controlsRow!.contains(screen.getByTestId("inline-create-agent-button"))).toBe(true);
expect(controlsRow!.contains(screen.getByTestId("inline-create-priority-select"))).toBe(true); expect(controlsRow!.contains(screen.getByTestId("inline-create-priority-select"))).toBe(true);
@@ -1210,18 +1208,16 @@ describe("InlineCreateCard button visibility when collapsed", () => {
expect(controlsRow!.contains(saveButton)).toBe(false); expect(controlsRow!.contains(saveButton)).toBe(false);
}); });
it("Plan and Subtask disabled state still works in consolidated controls", () => { it("Subtask disabled state still works in consolidated controls without Plan", () => {
renderCard(); renderCard();
expandCard(); expandCard();
const textarea = screen.getByPlaceholderText("What needs to be done?"); const textarea = screen.getByPlaceholderText("What needs to be done?");
// Buttons should be disabled when description is empty expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect((screen.getByTestId("plan-button") as HTMLButtonElement).disabled).toBe(true);
expect((screen.getByTestId("subtask-button") as HTMLButtonElement).disabled).toBe(true); expect((screen.getByTestId("subtask-button") as HTMLButtonElement).disabled).toBe(true);
// Type something — buttons should become enabled
fireEvent.change(textarea, { target: { value: "Some task" } }); fireEvent.change(textarea, { target: { value: "Some task" } });
expect((screen.getByTestId("plan-button") as HTMLButtonElement).disabled).toBe(false); expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect((screen.getByTestId("subtask-button") as HTMLButtonElement).disabled).toBe(false); expect((screen.getByTestId("subtask-button") as HTMLButtonElement).disabled).toBe(false);
}); });
}); });

View File

@@ -376,7 +376,6 @@ const QUICK_ENTRY_ACTION_BUTTONS = [
["Fast", "quick-entry-fast-toggle"], ["Fast", "quick-entry-fast-toggle"],
["GitHub", "quick-entry-github-toggle"], ["GitHub", "quick-entry-github-toggle"],
["Priority", "quick-entry-priority-button"], ["Priority", "quick-entry-priority-button"],
["Plan", "plan-button"],
["Subtask", "subtask-button"], ["Subtask", "subtask-button"],
["Refine", "refine-button"], ["Refine", "refine-button"],
["Deps", "quick-entry-deps"], ["Deps", "quick-entry-deps"],
@@ -739,19 +738,12 @@ describe("QuickEntryBox", () => {
expect(document.activeElement).not.toBe(textarea); expect(document.activeElement).not.toBe(textarea);
}); });
it("does not auto-focus after Plan or Subtask handoff reset the form", async () => { it("does not auto-focus after Subtask handoff resets the form", async () => {
mockDesktopViewport(); mockDesktopViewport();
const onPlanningMode = vi.fn();
const onSubtaskBreakdown = vi.fn(); const onSubtaskBreakdown = vi.fn();
renderQuickEntryBox({ onPlanningMode, onSubtaskBreakdown }); renderQuickEntryBox({ onSubtaskBreakdown });
let textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement; let textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement;
fireEvent.change(textarea, { target: { value: "Plan this" } });
fireEvent.click(screen.getByTestId("plan-button"));
await flushPendingTimers();
expect(onPlanningMode).toHaveBeenCalledWith("Plan this");
expect(document.activeElement).not.toBe(textarea);
fireEvent.change(textarea, { target: { value: "Break this down" } }); fireEvent.change(textarea, { target: { value: "Break this down" } });
fireEvent.click(screen.getByTestId("subtask-button")); fireEvent.click(screen.getByTestId("subtask-button"));
await flushPendingTimers(); await flushPendingTimers();
@@ -1067,7 +1059,7 @@ describe("QuickEntryBox", () => {
} }
}); });
it("does not fire disabled button actions via touch", async () => { it("does not render a disabled Plan button action via touch", async () => {
const onPlanningMode = vi.fn(); const onPlanningMode = vi.fn();
mockMobileViewport(); mockMobileViewport();
renderQuickEntryBox({ onPlanningMode }); renderQuickEntryBox({ onPlanningMode });
@@ -1075,13 +1067,9 @@ describe("QuickEntryBox", () => {
const textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement; const textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement;
textarea.focus(); textarea.focus();
expect(document.activeElement).toBe(textarea); expect(document.activeElement).toBe(textarea);
const planButton = screen.getByTestId("plan-button");
expect(planButton).toBeDisabled();
const { preventDefaultSpy } = fireCancelableTouchStart(planButton);
expect(preventDefaultSpy).not.toHaveBeenCalled();
fireEvent(planButton, new Event("touchend", { bubbles: true, cancelable: true }));
expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /^Plan$/i })).not.toBeInTheDocument();
expect(onPlanningMode).not.toHaveBeenCalled(); expect(onPlanningMode).not.toHaveBeenCalled();
expect(document.activeElement).toBe(textarea); expect(document.activeElement).toBe(textarea);
}); });
@@ -1110,9 +1098,8 @@ describe("QuickEntryBox", () => {
vi.mocked(fetchSettings).mockResolvedValueOnce({ vi.mocked(fetchSettings).mockResolvedValueOnce({
githubTrackingEnabledByDefault: true, githubTrackingEnabledByDefault: true,
} as any); } as any);
const onPlanningMode = vi.fn();
const onSubtaskBreakdown = vi.fn(); const onSubtaskBreakdown = vi.fn();
const result = renderQuickEntryBox({ onPlanningMode, onSubtaskBreakdown }); const result = renderQuickEntryBox({ onSubtaskBreakdown });
expandQuickEntry(); expandQuickEntry();
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId("quick-entry-github-toggle")).not.toBeDisabled(); expect(screen.getByTestId("quick-entry-github-toggle")).not.toBeDisabled();
@@ -1124,7 +1111,7 @@ describe("QuickEntryBox", () => {
textarea.blur(); textarea.blur();
fireEvent.blur(textarea); fireEvent.blur(textarea);
expect(document.activeElement).not.toBe(textarea); expect(document.activeElement).not.toBe(textarea);
return { ...result, textarea, onPlanningMode, onSubtaskBreakdown }; return { ...result, textarea, onSubtaskBreakdown };
} }
function fireCancelableTouchStart(target: Element) { function fireCancelableTouchStart(target: Element) {
@@ -1180,9 +1167,6 @@ describe("QuickEntryBox", () => {
case "refine-button": case "refine-button":
expect(await screen.findByTestId("refine-clarify")).toBeTruthy(); expect(await screen.findByTestId("refine-clarify")).toBeTruthy();
break; break;
case "plan-button":
expect(helpers.onPlanningMode).toHaveBeenCalledWith("Adjust options without keyboard");
break;
case "subtask-button": case "subtask-button":
expect(helpers.onSubtaskBreakdown).toHaveBeenCalledWith("Adjust options without keyboard"); expect(helpers.onSubtaskBreakdown).toHaveBeenCalledWith("Adjust options without keyboard");
break; break;
@@ -1622,7 +1606,7 @@ describe("QuickEntryBox", () => {
{ id: "wf-review-copy", name: "Review", columns: [] }, { id: "wf-review-copy", name: "Review", columns: [] },
]; ];
it("defaults to the provided workflow, changes selection, and passes it to Save/Plan/Subtask", async () => { it("defaults to the provided workflow, changes selection, and passes it to Save/Subtask while omitting Plan", async () => {
vi.mocked(fetchWorkflowOptionalSteps).mockResolvedValue([]); vi.mocked(fetchWorkflowOptionalSteps).mockResolvedValue([]);
const onCreate = vi.fn().mockResolvedValue(CREATED_TASK); const onCreate = vi.fn().mockResolvedValue(CREATED_TASK);
const onPlanningMode = vi.fn(); const onPlanningMode = vi.fn();
@@ -1646,9 +1630,8 @@ describe("QuickEntryBox", () => {
clickSave(); clickSave();
await waitFor(() => expect(onCreate).toHaveBeenCalledWith(expect.objectContaining({ workflowId: "wf-default" }))); await waitFor(() => expect(onCreate).toHaveBeenCalledWith(expect.objectContaining({ workflowId: "wf-default" })));
fireEvent.change(screen.getByTestId("quick-entry-input"), { target: { value: "Plan in selected workflow" } }); expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId("plan-button")); expect(onPlanningMode).not.toHaveBeenCalled();
expect(onPlanningMode).toHaveBeenCalledWith("Plan in selected workflow", "wf-default");
fireEvent.change(screen.getByTestId("quick-entry-input"), { target: { value: "Subtask in selected workflow" } }); fireEvent.change(screen.getByTestId("quick-entry-input"), { target: { value: "Subtask in selected workflow" } });
fireEvent.click(screen.getByTestId("subtask-button")); fireEvent.click(screen.getByTestId("subtask-button"));
@@ -1943,19 +1926,17 @@ describe("QuickEntryBox", () => {
expect(screen.getByTestId("quick-entry-save")).toBeTruthy(); expect(screen.getByTestId("quick-entry-save")).toBeTruthy();
}); });
it("shows Plan and Subtask buttons when expanded", () => { it("shows Subtask but no Plan button when expanded", () => {
renderQuickEntryBox({}); renderQuickEntryBox({});
// Controls region starts expanded/visible // Controls region starts expanded/visible
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false); expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
// Type something
expandQuickEntry(); expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input"); const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Task to plan" } }); fireEvent.change(textarea, { target: { value: "Task to plan" } });
// Now the Plan and Subtask buttons should be visible expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(screen.getByTestId("plan-button")).toBeTruthy();
expect(screen.getByTestId("subtask-button")).toBeTruthy(); expect(screen.getByTestId("subtask-button")).toBeTruthy();
}); });
@@ -2389,24 +2370,6 @@ describe("QuickEntryBox", () => {
expect(secondPayload.executionMode).toBeUndefined(); expect(secondPayload.executionMode).toBeUndefined();
}); });
it("keeps Fast state after Plan handoff preserves the quick-add draft", async () => {
const onPlanningMode = vi.fn();
renderQuickEntryBox({ onPlanningMode });
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.click(screen.getByTestId("quick-entry-fast-toggle"));
fireEvent.change(textarea, { target: { value: "plan input" } });
fireEvent.click(screen.getByTestId("plan-button"));
await waitFor(() => {
expect(onPlanningMode).toHaveBeenCalled();
});
expandQuickEntry();
expect(screen.getByTestId("quick-entry-fast-toggle").getAttribute("aria-pressed")).toBe("true");
});
it("clears Fast state after Subtask flow reset", async () => { it("clears Fast state after Subtask flow reset", async () => {
const onSubtaskBreakdown = vi.fn(); const onSubtaskBreakdown = vi.fn();
renderQuickEntryBox({ onSubtaskBreakdown }); renderQuickEntryBox({ onSubtaskBreakdown });
@@ -2443,25 +2406,6 @@ describe("QuickEntryBox", () => {
expect(screen.getByTestId("quick-entry-priority-button").textContent).toContain("Normal"); expect(screen.getByTestId("quick-entry-priority-button").textContent).toContain("Normal");
}); });
it("keeps selected priority after Plan handoff preserves the quick-add draft", async () => {
const onPlanningMode = vi.fn();
renderQuickEntryBox({ onPlanningMode });
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "plan priority" } });
openPriorityMenu();
fireEvent.click(screen.getByTestId("quick-entry-priority-option-urgent"));
fireEvent.click(screen.getByTestId("plan-button"));
await waitFor(() => {
expect(onPlanningMode).toHaveBeenCalled();
});
expandQuickEntry();
expect(screen.getByTestId("quick-entry-priority-button").textContent).toContain("Urgent");
});
it("resets priority to normal after Subtask flow", async () => { it("resets priority to normal after Subtask flow", async () => {
const onSubtaskBreakdown = vi.fn(); const onSubtaskBreakdown = vi.fn();
renderQuickEntryBox({ onSubtaskBreakdown }); renderQuickEntryBox({ onSubtaskBreakdown });
@@ -2795,23 +2739,17 @@ describe("QuickEntryBox", () => {
}); });
}); });
it("renders Plan as text-only and preserves planning handoff", async () => { it("omits the quick-add Plan button while preserving the draft", () => {
const onPlanningMode = vi.fn(); const onPlanningMode = vi.fn();
renderQuickEntryBox({ onPlanningMode }); renderQuickEntryBox({ onPlanningMode });
expandQuickEntry(); expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement; const textarea = screen.getByTestId("quick-entry-input") as HTMLTextAreaElement;
const planButton = screen.getByTestId("plan-button");
expect(planButton).toHaveTextContent("Plan");
expect(planButton.querySelector("svg")).toBeNull();
fireEvent.change(textarea, { target: { value: " Plan this task " } }); fireEvent.change(textarea, { target: { value: " Plan this task " } });
fireEvent.click(planButton);
await waitFor(() => {
expect(onPlanningMode).toHaveBeenCalledWith("Plan this task");
});
expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /^Plan$/i })).not.toBeInTheDocument();
expect(onPlanningMode).not.toHaveBeenCalled();
expect(textarea.value).toBe(" Plan this task "); expect(textarea.value).toBe(" Plan this task ");
expect(localStorage.getItem(QUICK_ENTRY_STORAGE_KEY)).toBe(" Plan this task "); expect(localStorage.getItem(QUICK_ENTRY_STORAGE_KEY)).toBe(" Plan this task ");
}); });
@@ -2833,10 +2771,7 @@ describe("QuickEntryBox", () => {
expect((textarea as HTMLTextAreaElement).value).toBe(""); expect((textarea as HTMLTextAreaElement).value).toBe("");
}); });
it.each([ it("passes selected workflow id through Subtask quick-entry handoff and omits Plan", async () => {
{ label: "Plan", buttonId: "plan-button", callbackProp: "onPlanningMode" as const },
{ label: "Subtask", buttonId: "subtask-button", callbackProp: "onSubtaskBreakdown" as const },
])("passes selected workflow id through %s quick-entry handoff", async ({ buttonId, callbackProp }) => {
const onPlanningMode = vi.fn(); const onPlanningMode = vi.fn();
const onSubtaskBreakdown = vi.fn(); const onSubtaskBreakdown = vi.fn();
renderQuickEntryBox({ onPlanningMode, onSubtaskBreakdown, workflowId: "WF-123" }); renderQuickEntryBox({ onPlanningMode, onSubtaskBreakdown, workflowId: "WF-123" });
@@ -2844,72 +2779,44 @@ describe("QuickEntryBox", () => {
const textarea = screen.getByTestId("quick-entry-input"); const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Create in custom workflow" } }); fireEvent.change(textarea, { target: { value: "Create in custom workflow" } });
fireEvent.click(screen.getByTestId(buttonId)); expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
fireEvent.click(screen.getByTestId("subtask-button"));
await waitFor(() => { await waitFor(() => {
expect(callbackProp === "onPlanningMode" ? onPlanningMode : onSubtaskBreakdown) expect(onSubtaskBreakdown).toHaveBeenCalledWith("Create in custom workflow", "WF-123");
.toHaveBeenCalledWith("Create in custom workflow", "WF-123");
}); });
expect(onPlanningMode).not.toHaveBeenCalled();
}); });
it("omits workflow id in legacy quick-entry handoff", async () => { it("omits legacy quick-entry Plan handoff", () => {
const onPlanningMode = vi.fn(); const onPlanningMode = vi.fn();
renderQuickEntryBox({ onPlanningMode }); renderQuickEntryBox({ onPlanningMode });
expandQuickEntry(); expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input"); const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Create with default workflow" } }); fireEvent.change(textarea, { target: { value: "Create with default workflow" } });
fireEvent.click(screen.getByTestId("plan-button"));
await waitFor(() => { expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(onPlanningMode).toHaveBeenCalledWith("Create with default workflow"); expect(onPlanningMode).not.toHaveBeenCalled();
});
expect(onPlanningMode.mock.calls[0]).toHaveLength(1);
}); });
it("disables Plan and Subtask buttons when description is empty", () => { it("omits Plan and keeps Subtask disabled when description is empty", () => {
renderQuickEntryBox({}); renderQuickEntryBox({});
expandQuickEntry(); expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input"); const textarea = screen.getByTestId("quick-entry-input");
// Type something first to make buttons appear
fireEvent.change(textarea, { target: { value: "Some task" } }); fireEvent.change(textarea, { target: { value: "Some task" } });
const planButton = screen.getByTestId("plan-button") as HTMLButtonElement; expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
const subtaskButton = screen.getByTestId("subtask-button") as HTMLButtonElement; const subtaskButton = screen.getByTestId("subtask-button") as HTMLButtonElement;
// Buttons should be enabled when there's content
expect(planButton.disabled).toBe(false);
expect(subtaskButton.disabled).toBe(false); expect(subtaskButton.disabled).toBe(false);
// Clear the input
fireEvent.change(textarea, { target: { value: "" } }); fireEvent.change(textarea, { target: { value: "" } });
// Buttons should now be disabled (or hidden since controls collapse) expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
// Since the controls might hide when empty, we check if they exist and are disabled expect((screen.getByTestId("subtask-button") as HTMLButtonElement).disabled).toBe(true);
const updatedPlanButton = screen.queryByTestId("plan-button") as HTMLButtonElement | null;
if (updatedPlanButton) {
expect(updatedPlanButton.disabled).toBe(true);
}
}); });
it("Plan button prevents textarea blur on mousedown", () => {
renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Task to plan" } });
// Get plan button and trigger mousedown (prevents blur)
const planButton = screen.getByTestId("plan-button");
fireEvent.mouseDown(planButton);
// Trigger blur on textarea
fireEvent.blur(textarea);
// Controls should still be visible immediately after blur
expect(screen.getByTestId("plan-button")).toBeTruthy();
});
it("Subtask button prevents textarea blur on mousedown", () => { it("Subtask button prevents textarea blur on mousedown", () => {
renderQuickEntryBox({}); renderQuickEntryBox({});
@@ -2929,24 +2836,18 @@ describe("QuickEntryBox", () => {
expect(screen.getByTestId("subtask-button")).toBeTruthy(); expect(screen.getByTestId("subtask-button")).toBeTruthy();
}); });
it("shows toast when Plan clicked with empty description", () => { it("does not leave a Plan tooltip or click target when description is empty", () => {
const addToast = vi.fn(); const addToast = vi.fn();
renderQuickEntryBox({ addToast }); renderQuickEntryBox({ addToast });
expandQuickEntry(); expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input"); const textarea = screen.getByTestId("quick-entry-input");
// Type something first to make buttons appear
fireEvent.change(textarea, { target: { value: "Some task" } }); fireEvent.change(textarea, { target: { value: "Some task" } });
// Clear input
fireEvent.change(textarea, { target: { value: "" } }); fireEvent.change(textarea, { target: { value: "" } });
// Button should be hidden when input is empty (controls collapse) expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
const planButton = screen.queryByTestId("plan-button"); expect(screen.queryByTitle("Open planning mode with current description")).not.toBeInTheDocument();
if (planButton) { expect(addToast).not.toHaveBeenCalled();
// If somehow visible, it should be disabled
expect((planButton as HTMLButtonElement).disabled).toBe(true);
}
}); });
it("includes all three selected model pairs in submit payload", async () => { it("includes all three selected model pairs in submit payload", async () => {
@@ -3823,7 +3724,7 @@ describe("QuickEntryBox", () => {
expandQuickEntry(); expandQuickEntry();
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false); expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
expect(screen.getByTestId("plan-button")).toBeTruthy(); expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(screen.getByTestId("subtask-button")).toBeTruthy(); expect(screen.getByTestId("subtask-button")).toBeTruthy();
expect(screen.getByTestId("refine-button")).toBeTruthy(); expect(screen.getByTestId("refine-button")).toBeTruthy();
expect(screen.getByTestId("quick-entry-deps")).toBeTruthy(); expect(screen.getByTestId("quick-entry-deps")).toBeTruthy();
@@ -3844,14 +3745,14 @@ describe("QuickEntryBox", () => {
}); });
describe("Consolidated actions layout (FN-781, FN-1088)", () => { describe("Consolidated actions layout (FN-781, FN-1088)", () => {
it("renders Plan, Subtask, and Refine in actions area inside controls panel", () => { it("renders Subtask and Refine in actions area without a Plan shell", () => {
renderQuickEntryBox({}); renderQuickEntryBox({});
expandQuickEntry(); expandQuickEntry();
expect(screen.getByTestId("quick-entry-actions")).toBeTruthy(); expect(screen.getByTestId("quick-entry-actions")).toBeTruthy();
const actionsContainer = screen.getByTestId("quick-entry-actions"); const actionsContainer = screen.getByTestId("quick-entry-actions");
expect(actionsContainer.contains(screen.getByTestId("plan-button"))).toBe(true); expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(actionsContainer.contains(screen.getByTestId("subtask-button"))).toBe(true); expect(actionsContainer.contains(screen.getByTestId("subtask-button"))).toBe(true);
expect(actionsContainer.contains(screen.getByTestId("refine-button"))).toBe(true); expect(actionsContainer.contains(screen.getByTestId("refine-button"))).toBe(true);
}); });
@@ -3863,7 +3764,7 @@ describe("QuickEntryBox", () => {
const actionsContainer = screen.getByTestId("quick-entry-actions"); const actionsContainer = screen.getByTestId("quick-entry-actions");
expect(screen.queryByTestId("subtask-button")).not.toBeInTheDocument(); expect(screen.queryByTestId("subtask-button")).not.toBeInTheDocument();
expect(screen.queryByTitle("Break down into AI-generated subtasks")).not.toBeInTheDocument(); expect(screen.queryByTitle("Break down into AI-generated subtasks")).not.toBeInTheDocument();
expect(actionsContainer.contains(screen.getByTestId("plan-button"))).toBe(true); expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(actionsContainer.contains(screen.getByTestId("refine-button"))).toBe(true); expect(actionsContainer.contains(screen.getByTestId("refine-button"))).toBe(true);
}); });
@@ -3884,17 +3785,6 @@ describe("QuickEntryBox", () => {
expect(screen.getByTestId("quick-entry-save")).toBeTruthy(); expect(screen.getByTestId("quick-entry-save")).toBeTruthy();
}); });
it("Plan button disabled state still works in actions area", () => {
renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
expect((screen.getByTestId("plan-button") as HTMLButtonElement).disabled).toBe(true);
fireEvent.change(textarea, { target: { value: "Some task" } });
expect((screen.getByTestId("plan-button") as HTMLButtonElement).disabled).toBe(false);
});
it("keeps all task creation controls together when disclosure is expanded", () => { it("keeps all task creation controls together when disclosure is expanded", () => {
renderQuickEntryBox({}); renderQuickEntryBox({});
expandQuickEntry(); expandQuickEntry();
@@ -3904,14 +3794,14 @@ describe("QuickEntryBox", () => {
const controlsPanel = document.getElementById("quick-entry-controls"); const controlsPanel = document.getElementById("quick-entry-controls");
expect(controlsPanel?.hasAttribute("hidden")).toBe(false); expect(controlsPanel?.hasAttribute("hidden")).toBe(false);
expect(screen.getByTestId("plan-button")).toBeTruthy(); expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(screen.getByTestId("subtask-button")).toBeTruthy(); expect(screen.getByTestId("subtask-button")).toBeTruthy();
expect(screen.getByTestId("refine-button")).toBeTruthy(); expect(screen.getByTestId("refine-button")).toBeTruthy();
expect(screen.getByTestId("quick-entry-deps")).toBeTruthy(); expect(screen.getByTestId("quick-entry-deps")).toBeTruthy();
expect(screen.getByTestId("quick-entry-models")).toBeTruthy(); expect(screen.getByTestId("quick-entry-models")).toBeTruthy();
expect(screen.getByTestId("quick-entry-save")).toBeTruthy(); expect(screen.getByTestId("quick-entry-save")).toBeTruthy();
expect(controlsPanel?.contains(screen.getByTestId("plan-button"))).toBe(true); expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(controlsPanel?.contains(screen.getByTestId("subtask-button"))).toBe(true); expect(controlsPanel?.contains(screen.getByTestId("subtask-button"))).toBe(true);
expect(controlsPanel?.contains(screen.getByTestId("refine-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-deps"))).toBe(true);
@@ -4261,13 +4151,13 @@ describe("QuickEntryBox", () => {
expect(modelsButton.className).toContain("btn"); expect(modelsButton.className).toContain("btn");
}); });
it("keeps Plan, Subtask, and Refine buttons in touch-target button classes", () => { it("keeps Subtask and Refine buttons in touch-target classes without Plan", () => {
vi.spyOn(window, "innerWidth", "get").mockReturnValue(375); vi.spyOn(window, "innerWidth", "get").mockReturnValue(375);
renderQuickEntryBox({}); renderQuickEntryBox({});
expandQuickEntry(); expandQuickEntry();
expect(screen.getByRole("button", { name: /Plan/i }).className).toContain("btn"); expect(screen.queryByRole("button", { name: /^Plan$/i })).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: /Subtask/i }).className).toContain("btn"); expect(screen.getByRole("button", { name: /Subtask/i }).className).toContain("btn");
expect(screen.getByRole("button", { name: /Refine/i }).className).toContain("btn"); expect(screen.getByRole("button", { name: /Refine/i }).className).toContain("btn");
}); });
@@ -4304,7 +4194,7 @@ describe("QuickEntryBox", () => {
expect(rect.right).toBeLessThanOrEqual(viewportWidth); expect(rect.right).toBeLessThanOrEqual(viewportWidth);
}); });
it("responds to clicks for toggle, plan, subtask, refine, and deps", async () => { it("responds to clicks for toggle, subtask, refine, and deps without Plan", async () => {
const onPlanningMode = vi.fn(); const onPlanningMode = vi.fn();
const onSubtaskBreakdown = vi.fn(); const onSubtaskBreakdown = vi.fn();
@@ -4322,8 +4212,8 @@ describe("QuickEntryBox", () => {
expect(toggle).toHaveAttribute("aria-expanded", "true"); expect(toggle).toHaveAttribute("aria-expanded", "true");
fireEvent.change(input, { target: { value: "Mobile interaction task" } }); fireEvent.change(input, { target: { value: "Mobile interaction task" } });
fireEvent.click(screen.getByRole("button", { name: /Plan/i })); expect(screen.queryByRole("button", { name: /^Plan$/i })).not.toBeInTheDocument();
expect(onPlanningMode).toHaveBeenCalledWith("Mobile interaction task"); expect(onPlanningMode).not.toHaveBeenCalled();
ensureExpanded(); ensureExpanded();
fireEvent.change(input, { target: { value: "Break this down" } }); fireEvent.change(input, { target: { value: "Break this down" } });

View File

@@ -607,7 +607,7 @@ describe("InlineCreateCard mobile", () => {
expectRuleToContain(mobileSection, ".inline-create-priority-select", "min-height: 36px;"); expectRuleToContain(mobileSection, ".inline-create-priority-select", "min-height: 36px;");
}); });
it("renders Plan and Subtask buttons when expanded", () => { it("renders Subtask but no Plan button when expanded", () => {
render( render(
<InlineCreateCard <InlineCreateCard
tasks={[]} tasks={[]}
@@ -615,13 +615,15 @@ describe("InlineCreateCard mobile", () => {
onCancel={vi.fn()} onCancel={vi.fn()}
addToast={vi.fn()} addToast={vi.fn()}
availableModels={[]} availableModels={[]}
onPlanningMode={vi.fn()}
onSubtaskBreakdown={vi.fn()} onSubtaskBreakdown={vi.fn()}
/>, />,
); );
fireEvent.click(screen.getByTestId("inline-create-toggle")); fireEvent.click(screen.getByTestId("inline-create-toggle"));
expect(screen.getByRole("button", { name: "Plan" })).toBeTruthy(); expect(screen.queryByTestId("plan-button")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Plan" })).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "Subtask" })).toBeTruthy(); expect(screen.getByRole("button", { name: "Subtask" })).toBeTruthy();
}); });