feat(FN-973): move plan/subtask/refine buttons to disclosure panel in QuickEntryBox

- Redesign QuickEntryBox to use a collapsible disclosure panel for plan, subtask, and AI refine actions
- Add disclosure toggle button (lightbulb icon) that expands/collapses the action panel
- Move dependency picker and model overrides into the disclosure panel
- Update ListView and QuickEntryBox tests to cover new disclosure panel behavior
- Add CSS styles for disclosure panel transitions and layout
This commit is contained in:
gsxdsm
2026-04-05 11:07:58 -07:00
parent 56dc5e8d3e
commit 77b960427c
4 changed files with 151 additions and 134 deletions

View File

@@ -732,111 +732,111 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
{isDisclosureExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</button>
</div>
{/* AI-assisted refinement actions — always visible when expanded */}
{isExpanded && !isSubmitting && (
<div className="quick-entry-description-actions" data-testid="quick-entry-description-actions">
<button
type="button"
className="btn btn-sm"
onClick={handlePlanClick}
onMouseDown={(e) => e.preventDefault()}
disabled={!description.trim()}
data-testid="plan-button"
title="Open planning mode with current description"
>
<Lightbulb size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
Plan
</button>
<button
type="button"
className="btn btn-sm"
onClick={handleSubtaskClick}
onMouseDown={(e) => e.preventDefault()}
disabled={!description.trim()}
data-testid="subtask-button"
title="Break down into AI-generated subtasks"
>
<ListTree size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
Subtask
</button>
<div className="refine-trigger-wrap" ref={refineMenuRef}>
<button
type="button"
className={`btn btn-sm refine-button ${isRefining ? "refine-button--loading" : ""}`}
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="Refine description with AI"
>
<Sparkles size={12} style={{ verticalAlign: "middle" }} />
{isRefining ? "Refining..." : "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">Clarify</div>
<div className="refine-menu-item-desc">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">Add details</div>
<div className="refine-menu-item-desc">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">Expand</div>
<div className="refine-menu-item-desc">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">Simplify</div>
<div className="refine-menu-item-desc">Simplify and make more concise</div>
</div>
</div>,
portalRoot,
)}
</div>
</div>
)}
<div
id="quick-entry-controls"
className="quick-entry-controls"
hidden={!showExpandedControls}
aria-hidden={!showExpandedControls}
>
{/* Plan, Subtask, Refine — consolidated action buttons */}
{showExpandedControls && !isSubmitting && (
<div className="quick-entry-actions" data-testid="quick-entry-actions">
<button
type="button"
className="btn btn-sm"
onClick={handlePlanClick}
onMouseDown={(e) => e.preventDefault()}
disabled={!description.trim()}
data-testid="plan-button"
title="Open planning mode with current description"
>
<Lightbulb size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
Plan
</button>
<button
type="button"
className="btn btn-sm"
onClick={handleSubtaskClick}
onMouseDown={(e) => e.preventDefault()}
disabled={!description.trim()}
data-testid="subtask-button"
title="Break down into AI-generated subtasks"
>
<ListTree size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
Subtask
</button>
<div className="refine-trigger-wrap" ref={refineMenuRef}>
<button
type="button"
className={`btn btn-sm refine-button ${isRefining ? "refine-button--loading" : ""}`}
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="Refine description with AI"
>
<Sparkles size={12} style={{ verticalAlign: "middle" }} />
{isRefining ? "Refining..." : "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">Clarify</div>
<div className="refine-menu-item-desc">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">Add details</div>
<div className="refine-menu-item-desc">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">Expand</div>
<div className="refine-menu-item-desc">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">Simplify</div>
<div className="refine-menu-item-desc">Simplify and make more concise</div>
</div>
</div>,
portalRoot,
)}
</div>
</div>
)}
<div className="quick-entry-controls-left">
{/* Deps button */}
<div className="dep-trigger-wrap">

View File

@@ -1463,11 +1463,7 @@ describe("ListView Quick Entry", () => {
const toggleButton = screen.getByTestId("quick-entry-toggle");
fireEvent.click(toggleButton);
// Open the actions dropdown to access Deps/Models/Save
const actionsButton = screen.getByTestId("quick-entry-actions-button");
fireEvent.click(actionsButton);
// Model selector button should be visible
// Model selector button should be directly visible in the disclosure panel
const modelButton = await screen.findByTestId("quick-entry-models-button");
expect(modelButton).toBeDefined();
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
@@ -1484,11 +1480,7 @@ describe("ListView Quick Entry", () => {
const toggleButton = screen.getByTestId("quick-entry-toggle");
fireEvent.click(toggleButton);
// Open the actions dropdown to access Deps/Models/Save
const actionsButton = screen.getByTestId("quick-entry-actions-button");
fireEvent.click(actionsButton);
// Dependency selector button should be visible
// Dependency selector button should be directly visible in the disclosure panel
const depsButton = await screen.findByTestId("quick-entry-deps-button");
expect(depsButton).toBeDefined();
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
@@ -2084,10 +2076,7 @@ describe("ListView - Bulk Selection", () => {
const toggleButton = screen.getByTestId("quick-entry-toggle");
fireEvent.click(toggleButton);
// Open the actions dropdown to access Deps/Models/Save
const actionsButton = screen.getByTestId("quick-entry-actions-button");
fireEvent.click(actionsButton);
// Model selector button is directly visible in the disclosure panel
const modelButton = await screen.findByTestId("quick-entry-models-button");
fireEvent.click(modelButton);

View File

@@ -1650,11 +1650,11 @@ describe("QuickEntryBox", () => {
// Now controls should be visible
expect(document.getElementById("quick-entry-controls")?.hasAttribute("hidden")).toBe(false);
// Deps/Models/Save should be directly accessible (no actions dropdown needed)
// Deps/Models/Save should be directly accessible
expect(screen.getByTestId("quick-entry-deps-button")).toBeTruthy();
expect(screen.getByTestId("quick-entry-models-button")).toBeTruthy();
expect(screen.getByTestId("save-button")).toBeTruthy();
// Plan/Subtask are always visible in description-actions when expanded
// Plan/Subtask are in the actions area inside the disclosure panel
expect(screen.getByTestId("plan-button")).toBeTruthy();
expect(screen.getByTestId("subtask-button")).toBeTruthy();
});
@@ -1688,57 +1688,57 @@ describe("QuickEntryBox", () => {
});
});
describe("Description-adjacent actions layout (FN-781)", () => {
it("renders Plan, Subtask, and Refine in description-actions area when expanded", () => {
describe("Consolidated actions layout (FN-781, FN-973)", () => {
it("renders Plan, Subtask, and Refine in actions area inside controls panel", () => {
renderQuickEntryBox({});
expandQuickEntry();
// The description-actions container should exist
expect(screen.getByTestId("quick-entry-description-actions")).toBeTruthy();
// The actions container should exist inside the controls panel
expect(screen.getByTestId("quick-entry-actions")).toBeTruthy();
// Plan, Subtask, and Refine buttons should be inside it
const actionsContainer = screen.getByTestId("quick-entry-description-actions");
const actionsContainer = screen.getByTestId("quick-entry-actions");
expect(actionsContainer.contains(screen.getByTestId("plan-button"))).toBe(true);
expect(actionsContainer.contains(screen.getByTestId("subtask-button"))).toBe(true);
expect(actionsContainer.contains(screen.getByTestId("refine-button"))).toBe(true);
});
it("does not render description-actions when not expanded", () => {
it("does not render actions when not expanded", () => {
renderQuickEntryBox({});
// Description actions should not exist when collapsed
expect(screen.queryByTestId("quick-entry-description-actions")).toBeNull();
// Actions should not exist when collapsed
expect(screen.queryByTestId("quick-entry-actions")).toBeNull();
});
it("Save button is in the controls panel, not description-actions", () => {
it("Save button is in the controls panel, not actions area", () => {
renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Task to save" } });
const actionsContainer = screen.getByTestId("quick-entry-description-actions");
const actionsContainer = screen.getByTestId("quick-entry-actions");
const saveButton = screen.getByTestId("save-button");
// Save button should NOT be in the description-actions area (it's in the controls panel)
// Save button should NOT be in the actions area (it's in the controls-left panel)
expect(actionsContainer.contains(saveButton)).toBe(false);
});
it("Deps and Models buttons are in the controls panel, not description-actions", () => {
it("Deps and Models buttons are in the controls panel, not actions area", () => {
renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Task text" } });
const actionsContainer = screen.getByTestId("quick-entry-description-actions");
const actionsContainer = screen.getByTestId("quick-entry-actions");
const depsButton = screen.getByTestId("quick-entry-deps-button");
const modelsButton = screen.getByTestId("quick-entry-models-button");
// Deps and Models should NOT be in the description-actions area (they're in the controls panel)
// Deps and Models should NOT be in the actions area (they're in the controls-left panel)
expect(actionsContainer.contains(depsButton)).toBe(false);
expect(actionsContainer.contains(modelsButton)).toBe(false);
});
it("Plan button disabled state still works when in description-actions", () => {
it("Plan button disabled state still works in actions area", () => {
renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
@@ -1750,6 +1750,35 @@ describe("QuickEntryBox", () => {
fireEvent.change(textarea, { target: { value: "Some task" } });
expect((screen.getByTestId("plan-button") as HTMLButtonElement).disabled).toBe(false);
});
it("shows Plan/Subtask/Refine together with Deps/Models/Save when disclosure is expanded and text entered", () => {
renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "A task with all features" } });
// All 6 buttons should be visible inside the controls panel
const controlsPanel = document.getElementById("quick-entry-controls");
expect(controlsPanel?.hasAttribute("hidden")).toBe(false);
// Plan/Subtask/Refine are in actions area
expect(screen.getByTestId("plan-button")).toBeTruthy();
expect(screen.getByTestId("subtask-button")).toBeTruthy();
expect(screen.getByTestId("refine-button")).toBeTruthy();
// Deps/Models/Save are in controls-left area
expect(screen.getByTestId("quick-entry-deps-button")).toBeTruthy();
expect(screen.getByTestId("quick-entry-models-button")).toBeTruthy();
expect(screen.getByTestId("save-button")).toBeTruthy();
// All should be inside the same controls panel
expect(controlsPanel?.contains(screen.getByTestId("plan-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("quick-entry-deps-button"))).toBe(true);
expect(controlsPanel?.contains(screen.getByTestId("quick-entry-models-button"))).toBe(true);
expect(controlsPanel?.contains(screen.getByTestId("save-button"))).toBe(true);
});
});
describe("Preset selection through model menu", () => {

View File

@@ -12415,12 +12415,11 @@ html .column.drag-over * {
flex-wrap: wrap;
}
/* AI-assisted description actions — sits between textarea and expanded controls */
.quick-entry-description-actions {
/* Consolidated action buttons (Plan, Subtask, Refine) inside disclosure panel */
.quick-entry-actions {
display: flex;
align-items: center;
gap: 6px;
margin-top: 4px;
gap: 8px;
flex-wrap: wrap;
}