feat(FN-770): add quick-add favorite models to task creation surfaces
- Wire favorite model state into QuickEntryBox with star toggle and dropdown integration - Add favorite model selector to Board and Column quick-add components - Pass favorite models through ListView and App context to entry surfaces - Add regression tests for QuickEntryBox, Board, Column, and ListView favorite behavior - Update dashboard README to document quick-add favorite model support - Remove unused recovery-policy module and dead code from executor/triage/scheduler - Clean up unused types from core and simplify store methods
This commit is contained in:
@@ -43,7 +43,7 @@ AI-guided interactive planning for creating well-specified tasks from high-level
|
||||
- **Inline Editing**: Quick-edit a task's description directly on the board for Triage and Todo columns. Double-click a card or use the pencil icon — visible on hover for desktop, always visible on mobile for touch accessibility. Inline editing changes only the description; the title is preserved. To edit both title and description, use the task detail modal.
|
||||
- **Task Detail Editing**: Edit task title and description directly in the task detail modal. Click the pencil icon in the modal header (available for Triage and Todo tasks) to enter edit mode.
|
||||
- **List View**: Alternative tabular view for tasks with sorting and filtering. The "Hide Done" toggle hides both Done and Archived tasks for an active-work-only view.
|
||||
- **Model Selection at Creation**: Choose executor and validator AI models while creating tasks from the board or list view, or leave them unset to use the global defaults.
|
||||
- **Model Selection at Creation**: Choose executor and validator AI models while creating tasks from the board or list view, or leave them unset to use the global defaults. Quick-add model dropdowns in both the board triage column and the list view honor saved favorite providers and pinned models, matching the rest of the dashboard model UI.
|
||||
- **Layered Model Dropdowns**: Shared model combobox menus render in a top-level portal attached to `document.body`, so they stay above board columns and scrollable modal content instead of being clipped behind surrounding dashboard surfaces.
|
||||
- **Bulk Model Editing**: Update AI model configuration for multiple tasks at once in the list view. Select tasks via checkboxes (archived tasks excluded), then use the "Bulk Edit Models" toolbar to apply executor and/or validator model changes to all selected tasks. Selection persists in localStorage across page reloads.
|
||||
- **Task Details**: View full task specifications, agent logs, and attachments. The Agent Log tab header shows the effective executor and validator model names resolved from task-level overrides or project/global settings fallbacks, matching the same resolution order the engine uses at runtime.
|
||||
|
||||
@@ -514,6 +514,10 @@ function AppInner() {
|
||||
searchQuery={searchQuery}
|
||||
availableModels={availableModels}
|
||||
onOpenFilesForTask={handleOpenChangedFiles}
|
||||
favoriteProviders={favoriteProviders}
|
||||
favoriteModels={favoriteModels}
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
onToggleModelFavorite={handleToggleModelFavorite}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ interface BoardProps {
|
||||
*/
|
||||
onSubtaskBreakdown?: (description: string) => void;
|
||||
onOpenFilesForTask?: (taskId: string, worktree: string | undefined, column: string) => void;
|
||||
favoriteProviders?: string[];
|
||||
favoriteModels?: string[];
|
||||
onToggleFavorite?: (provider: string) => void;
|
||||
onToggleModelFavorite?: (modelId: string) => void;
|
||||
}
|
||||
|
||||
function sortTasksForColumn(tasks: Task[]): Task[] {
|
||||
@@ -54,7 +58,7 @@ function areTaskArraysEqual(previous: Task[], next: Task[]): boolean {
|
||||
return previous.every((task, index) => task === next[index]);
|
||||
}
|
||||
|
||||
export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenFilesForTask }: BoardProps) {
|
||||
export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenFilesForTask, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite }: BoardProps) {
|
||||
const [archivedCollapsed, setArchivedCollapsed] = useState(true);
|
||||
const { fetchBatch } = useBatchBadgeFetch(projectId);
|
||||
const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -164,6 +168,10 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onOpenDetai
|
||||
allTasks={filteredTasks}
|
||||
availableModels={availableModels}
|
||||
onOpenFilesForTask={onOpenFilesForTask}
|
||||
favoriteProviders={favoriteProviders}
|
||||
favoriteModels={favoriteModels}
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
onToggleModelFavorite={onToggleModelFavorite}
|
||||
{...(col === "triage" ? { onQuickCreate, onNewTask, onPlanningMode, onSubtaskBreakdown } : {})}
|
||||
{...(col === "in-review" ? { autoMerge, onToggleAutoMerge } : {})}
|
||||
{...(col === "done" ? { onArchiveAllDone } : {})}
|
||||
|
||||
@@ -47,9 +47,13 @@ interface ColumnProps {
|
||||
*/
|
||||
onSubtaskBreakdown?: (description: string) => void;
|
||||
onOpenFilesForTask?: (taskId: string, worktree: string | undefined, column: string) => void;
|
||||
favoriteProviders?: string[];
|
||||
favoriteModels?: string[];
|
||||
onToggleFavorite?: (provider: string) => void;
|
||||
onToggleModelFavorite?: (modelId: string) => void;
|
||||
}
|
||||
|
||||
function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, collapsed, onToggleCollapse, allTasks, availableModels, onPlanningMode, onSubtaskBreakdown, onOpenFilesForTask }: ColumnProps) {
|
||||
function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, collapsed, onToggleCollapse, allTasks, availableModels, onPlanningMode, onSubtaskBreakdown, onOpenFilesForTask, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite }: ColumnProps) {
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const [visibleTaskCount, setVisibleTaskCount] = useState(VISIBLE_TASKS_INITIAL);
|
||||
const countFlashing = useFlashOnIncrease(tasks.length);
|
||||
@@ -196,6 +200,10 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask,
|
||||
onPlanningMode={onPlanningMode}
|
||||
onSubtaskBreakdown={onSubtaskBreakdown}
|
||||
autoExpand={true}
|
||||
favoriteProviders={favoriteProviders}
|
||||
favoriteModels={favoriteModels}
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
onToggleModelFavorite={onToggleModelFavorite}
|
||||
/>
|
||||
)}
|
||||
{column === "in-progress" ? (
|
||||
|
||||
@@ -748,6 +748,10 @@ export function ListView({
|
||||
onPlanningMode={onPlanningMode}
|
||||
onSubtaskBreakdown={onSubtaskBreakdown}
|
||||
autoExpand={false}
|
||||
favoriteProviders={favoriteProviders}
|
||||
favoriteModels={favoriteModels}
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
onToggleModelFavorite={onToggleModelFavorite}
|
||||
/>
|
||||
</div>
|
||||
{filteredCount === 0 ? (
|
||||
|
||||
@@ -29,6 +29,26 @@ interface QuickEntryBoxProps {
|
||||
* Defaults to true for backward compatibility.
|
||||
*/
|
||||
autoExpand?: boolean;
|
||||
/**
|
||||
* Favorited provider IDs from shared app-level state.
|
||||
* When provided (alongside availableModels), the component uses these
|
||||
* instead of its own internal favorite state.
|
||||
*/
|
||||
favoriteProviders?: string[];
|
||||
/**
|
||||
* Favorited model IDs from shared app-level state.
|
||||
* When provided (alongside availableModels), the component uses these
|
||||
* instead of its own internal favorite state.
|
||||
*/
|
||||
favoriteModels?: string[];
|
||||
/**
|
||||
* Toggle favorite provider callback from shared app-level state.
|
||||
*/
|
||||
onToggleFavorite?: (provider: string) => void;
|
||||
/**
|
||||
* Toggle favorite model callback from shared app-level state.
|
||||
*/
|
||||
onToggleModelFavorite?: (modelId: string) => void;
|
||||
}
|
||||
|
||||
function getModelSelectionValue(provider?: string, modelId?: string): string {
|
||||
@@ -51,7 +71,7 @@ function parseModelSelection(value: string): { provider?: string; modelId?: stri
|
||||
};
|
||||
}
|
||||
|
||||
export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, onPlanningMode, onSubtaskBreakdown, autoExpand = true }: QuickEntryBoxProps) {
|
||||
export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, onPlanningMode, onSubtaskBreakdown, autoExpand = true, favoriteProviders: parentFavoriteProviders, favoriteModels: parentFavoriteModels, onToggleFavorite: parentToggleFavorite, onToggleModelFavorite: parentToggleModelFavorite }: QuickEntryBoxProps) {
|
||||
const [description, setDescription] = useState(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
return localStorage.getItem(STORAGE_KEY) || "";
|
||||
@@ -87,6 +107,10 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
const [isRefining, setIsRefining] = useState(false);
|
||||
const refineMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Use parent-provided favorites when available, otherwise internal state
|
||||
const effectiveFavoriteProviders = parentFavoriteProviders ?? favoriteProviders;
|
||||
const effectiveFavoriteModels = parentFavoriteModels ?? favoriteModels;
|
||||
|
||||
// If onCreate is not provided, the component is disabled
|
||||
const isDisabled = !onCreate;
|
||||
|
||||
@@ -106,8 +130,13 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
.then((response) => {
|
||||
if (!cancelled) {
|
||||
setLoadedModels(response.models);
|
||||
setFavoriteProviders(response.favoriteProviders);
|
||||
setFavoriteModels(response.favoriteModels);
|
||||
// Only set internal favorites when parent doesn't manage them
|
||||
if (!parentFavoriteProviders) {
|
||||
setFavoriteProviders(response.favoriteProviders);
|
||||
}
|
||||
if (!parentFavoriteModels) {
|
||||
setFavoriteModels(response.favoriteModels);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
@@ -374,6 +403,12 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
}, []);
|
||||
|
||||
const handleToggleFavorite = useCallback(async (provider: string) => {
|
||||
// Delegate to parent callback when available
|
||||
if (parentToggleFavorite) {
|
||||
parentToggleFavorite(provider);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFavorites = favoriteProviders;
|
||||
const isFavorite = currentFavorites.includes(provider);
|
||||
const newFavorites = isFavorite
|
||||
@@ -388,9 +423,15 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
// Revert on error
|
||||
setFavoriteProviders(currentFavorites);
|
||||
}
|
||||
}, [favoriteProviders, favoriteModels]);
|
||||
}, [favoriteProviders, favoriteModels, parentToggleFavorite]);
|
||||
|
||||
const handleToggleModelFavorite = useCallback(async (modelId: string) => {
|
||||
// Delegate to parent callback when available
|
||||
if (parentToggleModelFavorite) {
|
||||
parentToggleModelFavorite(modelId);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFavorites = favoriteModels;
|
||||
const isFavorite = currentFavorites.includes(modelId);
|
||||
const newFavorites = isFavorite
|
||||
@@ -405,7 +446,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
// Revert on error
|
||||
setFavoriteModels(currentFavorites);
|
||||
}
|
||||
}, [favoriteModels, favoriteProviders]);
|
||||
}, [favoriteModels, favoriteProviders, parentToggleModelFavorite]);
|
||||
|
||||
const handlePlanClick = useCallback(() => {
|
||||
const trimmed = description.trim();
|
||||
@@ -473,14 +514,19 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
try {
|
||||
const response = await fetchModels();
|
||||
setLoadedModels(response.models);
|
||||
setFavoriteProviders(response.favoriteProviders);
|
||||
setFavoriteModels(response.favoriteModels);
|
||||
// Only set internal favorites when parent doesn't manage them
|
||||
if (!parentFavoriteProviders) {
|
||||
setFavoriteProviders(response.favoriteProviders);
|
||||
}
|
||||
if (!parentFavoriteModels) {
|
||||
setFavoriteModels(response.favoriteModels);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setModelsError(err?.message || "Failed to load models");
|
||||
} finally {
|
||||
setModelsLoading(false);
|
||||
}
|
||||
}, [availableModels]);
|
||||
}, [availableModels, parentFavoriteProviders, parentFavoriteModels]);
|
||||
|
||||
// Show expanded controls based on disclosure state (user preference), not textarea focus
|
||||
const showExpandedControls = isDisclosureExpanded;
|
||||
@@ -713,9 +759,9 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
modelsLoading={modelsLoading}
|
||||
modelsError={modelsError}
|
||||
onRetry={loadModels}
|
||||
favoriteProviders={favoriteProviders}
|
||||
favoriteProviders={effectiveFavoriteProviders}
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
favoriteModels={favoriteModels}
|
||||
favoriteModels={effectiveFavoriteModels}
|
||||
onToggleModelFavorite={handleToggleModelFavorite}
|
||||
/>,
|
||||
document.body,
|
||||
|
||||
@@ -32,7 +32,7 @@ vi.mock("../../api", async (importOriginal) => {
|
||||
),
|
||||
loginProvider: vi.fn(() => Promise.resolve({ url: "https://auth.example.com/login" })),
|
||||
logoutProvider: vi.fn(() => Promise.resolve({ success: true })),
|
||||
fetchModels: vi.fn(() => Promise.resolve({ models: [], favoriteProviders: [] })),
|
||||
fetchModels: vi.fn(() => Promise.resolve({ models: [], favoriteProviders: [], favoriteModels: [] })),
|
||||
fetchGitRemotes: vi.fn(() => Promise.resolve([])),
|
||||
fetchAgents: vi.fn(() => Promise.resolve([])),
|
||||
fetchTaskDetail: vi.fn((id: string) => Promise.resolve({ id, title: `Task ${id}` })),
|
||||
|
||||
@@ -10,10 +10,10 @@ const columnRenderCounts: Record<string, number> = {};
|
||||
|
||||
// Mock child components so we only test Board's own rendering
|
||||
vi.mock("../Column", () => ({
|
||||
Column: React.memo(({ column, tasks, onToggleCollapse, availableModels }: { column: string; tasks: Task[]; onToggleCollapse?: () => void; availableModels?: unknown }) => {
|
||||
Column: React.memo(({ column, tasks, onToggleCollapse, availableModels, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite }: { column: string; tasks: Task[]; onToggleCollapse?: () => void; availableModels?: unknown; favoriteProviders?: string[]; favoriteModels?: string[]; onToggleFavorite?: (provider: string) => void; onToggleModelFavorite?: (modelId: string) => void }) => {
|
||||
columnRenderCounts[column] = (columnRenderCounts[column] ?? 0) + 1;
|
||||
return (
|
||||
<div data-testid={`column-${column}`} data-tasks={JSON.stringify(tasks)}>
|
||||
<div data-testid={`column-${column}`} data-tasks={JSON.stringify(tasks)} data-favorite-providers={JSON.stringify(favoriteProviders ?? [])} data-favorite-models={JSON.stringify(favoriteModels ?? [])} data-has-toggle-favorite={onToggleFavorite ? "yes" : "no"} data-has-toggle-model-favorite={onToggleModelFavorite ? "yes" : "no"}>
|
||||
{onToggleCollapse && <button onClick={onToggleCollapse}>toggle-{column}</button>}
|
||||
</div>
|
||||
);
|
||||
@@ -336,4 +336,41 @@ describe("Board", () => {
|
||||
const badge = document.querySelector(".board-project-context");
|
||||
expect(badge).toBeNull();
|
||||
});
|
||||
|
||||
describe("favorite model prop forwarding (FN-770)", () => {
|
||||
it("forwards favoriteProviders and favoriteModels to all columns", () => {
|
||||
const favoriteProviders = ["anthropic"];
|
||||
const favoriteModels = ["claude-sonnet-4-5"];
|
||||
const onToggleFavorite = vi.fn();
|
||||
const onToggleModelFavorite = vi.fn();
|
||||
|
||||
renderBoard({
|
||||
favoriteProviders,
|
||||
favoriteModels,
|
||||
onToggleFavorite,
|
||||
onToggleModelFavorite,
|
||||
});
|
||||
|
||||
// Every column should receive the favorite props
|
||||
for (const col of COLUMNS) {
|
||||
const columnEl = screen.getByTestId(`column-${col}`);
|
||||
expect(columnEl.getAttribute("data-favorite-providers")).toBe(JSON.stringify(favoriteProviders));
|
||||
expect(columnEl.getAttribute("data-favorite-models")).toBe(JSON.stringify(favoriteModels));
|
||||
expect(columnEl.getAttribute("data-has-toggle-favorite")).toBe("yes");
|
||||
expect(columnEl.getAttribute("data-has-toggle-model-favorite")).toBe("yes");
|
||||
}
|
||||
});
|
||||
|
||||
it("passes empty arrays for favorites when not provided", () => {
|
||||
renderBoard();
|
||||
|
||||
for (const col of COLUMNS) {
|
||||
const columnEl = screen.getByTestId(`column-${col}`);
|
||||
expect(columnEl.getAttribute("data-favorite-providers")).toBe("[]");
|
||||
expect(columnEl.getAttribute("data-favorite-models")).toBe("[]");
|
||||
expect(columnEl.getAttribute("data-has-toggle-favorite")).toBe("no");
|
||||
expect(columnEl.getAttribute("data-has-toggle-model-favorite")).toBe("no");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,15 @@ vi.mock("../WorktreeGroup", () => ({
|
||||
WorktreeGroup: () => <div />,
|
||||
}));
|
||||
vi.mock("../QuickEntryBox", () => ({
|
||||
QuickEntryBox: () => <div data-testid="quick-entry-box" />,
|
||||
QuickEntryBox: ({ favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite }: { favoriteProviders?: string[]; favoriteModels?: string[]; onToggleFavorite?: (provider: string) => void; onToggleModelFavorite?: (modelId: string) => void }) => (
|
||||
<div
|
||||
data-testid="quick-entry-box"
|
||||
data-favorite-providers={JSON.stringify(favoriteProviders ?? [])}
|
||||
data-favorite-models={JSON.stringify(favoriteModels ?? [])}
|
||||
data-has-toggle-favorite={onToggleFavorite ? "yes" : "no"}
|
||||
data-has-toggle-model-favorite={onToggleModelFavorite ? "yes" : "no"}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
vi.mock("lucide-react", () => ({
|
||||
Link: () => null,
|
||||
@@ -261,4 +269,47 @@ describe("Column same-column drop", () => {
|
||||
|
||||
expect(onMoveTask).toHaveBeenCalledWith("FN-001", "in-review");
|
||||
});
|
||||
|
||||
describe("favorite model prop forwarding (FN-770)", () => {
|
||||
it("forwards favoriteProviders, favoriteModels, and toggle callbacks to QuickEntryBox", () => {
|
||||
const onToggleFavorite = vi.fn();
|
||||
const onToggleModelFavorite = vi.fn();
|
||||
|
||||
render(
|
||||
<Column
|
||||
{...defaultProps}
|
||||
column="triage"
|
||||
tasks={[]}
|
||||
onQuickCreate={vi.fn().mockResolvedValue({})}
|
||||
favoriteProviders={["anthropic"]}
|
||||
favoriteModels={["claude-sonnet-4-5"]}
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
onToggleModelFavorite={onToggleModelFavorite}
|
||||
/>,
|
||||
);
|
||||
|
||||
const quickEntry = screen.getByTestId("quick-entry-box");
|
||||
expect(quickEntry.getAttribute("data-favorite-providers")).toBe(JSON.stringify(["anthropic"]));
|
||||
expect(quickEntry.getAttribute("data-favorite-models")).toBe(JSON.stringify(["claude-sonnet-4-5"]));
|
||||
expect(quickEntry.getAttribute("data-has-toggle-favorite")).toBe("yes");
|
||||
expect(quickEntry.getAttribute("data-has-toggle-model-favorite")).toBe("yes");
|
||||
});
|
||||
|
||||
it("passes empty favorites when props not provided", () => {
|
||||
render(
|
||||
<Column
|
||||
{...defaultProps}
|
||||
column="triage"
|
||||
tasks={[]}
|
||||
onQuickCreate={vi.fn().mockResolvedValue({})}
|
||||
/>,
|
||||
);
|
||||
|
||||
const quickEntry = screen.getByTestId("quick-entry-box");
|
||||
expect(quickEntry.getAttribute("data-favorite-providers")).toBe("[]");
|
||||
expect(quickEntry.getAttribute("data-favorite-models")).toBe("[]");
|
||||
expect(quickEntry.getAttribute("data-has-toggle-favorite")).toBe("no");
|
||||
expect(quickEntry.getAttribute("data-has-toggle-model-favorite")).toBe("no");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1973,4 +1973,42 @@ describe("ListView - Bulk Selection", () => {
|
||||
// The dropdown would need to be interacted with to enable the button
|
||||
// This test verifies the initial disabled state and button presence
|
||||
});
|
||||
|
||||
it("forwards favoriteProviders and favoriteModels to QuickEntryBox model modal (FN-770)", async () => {
|
||||
const availableModels = [
|
||||
{ provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", reasoning: true, contextWindow: 200000 },
|
||||
];
|
||||
const tasks = [createMockTask({ id: "FN-001" })];
|
||||
const onToggleFavorite = vi.fn();
|
||||
const onToggleModelFavorite = vi.fn();
|
||||
|
||||
render(
|
||||
<ListView
|
||||
tasks={tasks}
|
||||
onMoveTask={vi.fn()}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={mockAddToast}
|
||||
onQuickCreate={vi.fn().mockResolvedValue(undefined)}
|
||||
availableModels={availableModels}
|
||||
favoriteProviders={["anthropic"]}
|
||||
favoriteModels={["claude-sonnet-4-5"]}
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
onToggleModelFavorite={onToggleModelFavorite}
|
||||
/>
|
||||
);
|
||||
|
||||
// Expand the QuickEntryBox and open the model modal
|
||||
const toggleButton = screen.getByTestId("quick-entry-toggle");
|
||||
fireEvent.click(toggleButton);
|
||||
|
||||
const modelButton = await screen.findByTestId("quick-entry-models-button");
|
||||
fireEvent.click(modelButton);
|
||||
|
||||
// The real ModelSelectionModal should render via portal with data-testid
|
||||
const modal = await screen.findByTestId("model-selection-modal");
|
||||
expect(modal).toBeDefined();
|
||||
|
||||
// Verify the modal has content (models are loaded)
|
||||
expect(modal.textContent).toContain("Select Models");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,6 +95,8 @@ vi.mock("../ModelSelectionModal", () => ({
|
||||
modelsLoading,
|
||||
modelsError,
|
||||
onRetry,
|
||||
favoriteProviders,
|
||||
onToggleFavorite,
|
||||
favoriteModels,
|
||||
onToggleModelFavorite,
|
||||
}: {
|
||||
@@ -108,6 +110,8 @@ vi.mock("../ModelSelectionModal", () => ({
|
||||
modelsLoading: boolean;
|
||||
modelsError: string | null;
|
||||
onRetry: () => void;
|
||||
favoriteProviders?: string[];
|
||||
onToggleFavorite?: (provider: string) => void;
|
||||
favoriteModels?: string[];
|
||||
onToggleModelFavorite?: (modelId: string) => void;
|
||||
}) => {
|
||||
@@ -119,6 +123,8 @@ vi.mock("../ModelSelectionModal", () => ({
|
||||
<div data-testid="modal-props-validator-value">{validatorValue}</div>
|
||||
<div data-testid="modal-props-loading">{modelsLoading ? "loading" : "not-loading"}</div>
|
||||
<div data-testid="modal-props-error">{modelsError || "no-error"}</div>
|
||||
<div data-testid="modal-props-favorite-providers">{JSON.stringify(favoriteProviders ?? [])}</div>
|
||||
<div data-testid="modal-props-has-toggle-favorite">{onToggleFavorite ? "yes" : "no"}</div>
|
||||
<div data-testid="modal-props-favorite-models">{JSON.stringify(favoriteModels ?? [])}</div>
|
||||
<div data-testid="modal-props-has-toggle-model-favorite">{onToggleModelFavorite ? "yes" : "no"}</div>
|
||||
<button data-testid="modal-close" onClick={onClose}>
|
||||
@@ -612,6 +618,63 @@ describe("QuickEntryBox", () => {
|
||||
expect(screen.getByTestId("modal-props-has-toggle-model-favorite").textContent).toBe("yes");
|
||||
});
|
||||
|
||||
it("passes favoriteProviders and favoriteModels from parent props to ModelSelectionModal (regression FN-770)", () => {
|
||||
const parentToggleFavorite = vi.fn();
|
||||
const parentToggleModelFavorite = vi.fn();
|
||||
renderQuickEntryBox({
|
||||
favoriteProviders: ["anthropic"],
|
||||
favoriteModels: ["claude-sonnet-4-5"],
|
||||
onToggleFavorite: parentToggleFavorite,
|
||||
onToggleModelFavorite: parentToggleModelFavorite,
|
||||
});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with parent favorites" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
|
||||
expect(screen.getByTestId("modal-props-favorite-providers").textContent).toBe(JSON.stringify(["anthropic"]));
|
||||
expect(screen.getByTestId("modal-props-favorite-models").textContent).toBe(JSON.stringify(["claude-sonnet-4-5"]));
|
||||
expect(screen.getByTestId("modal-props-has-toggle-favorite").textContent).toBe("yes");
|
||||
expect(screen.getByTestId("modal-props-has-toggle-model-favorite").textContent).toBe("yes");
|
||||
});
|
||||
|
||||
it("delegates toggle favorite to parent callback when provided (regression FN-770)", () => {
|
||||
const parentToggleFavorite = vi.fn();
|
||||
const parentToggleModelFavorite = vi.fn();
|
||||
renderQuickEntryBox({
|
||||
favoriteProviders: ["anthropic"],
|
||||
favoriteModels: ["claude-sonnet-4-5"],
|
||||
onToggleFavorite: parentToggleFavorite,
|
||||
onToggleModelFavorite: parentToggleModelFavorite,
|
||||
});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with parent favorites" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
|
||||
// The modal has toggle callbacks; simulate using them
|
||||
expect(screen.getByTestId("modal-props-has-toggle-favorite").textContent).toBe("yes");
|
||||
expect(screen.getByTestId("modal-props-has-toggle-model-favorite").textContent).toBe("yes");
|
||||
});
|
||||
|
||||
it("falls back to internal favorites when parent props not provided (standalone mode)", () => {
|
||||
// availableModels is supplied but no favorite props — uses internal empty state
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Standalone task" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
|
||||
// When parent doesn't provide favorites, internal state is used (empty by default)
|
||||
expect(screen.getByTestId("modal-props-favorite-providers").textContent).toBe("[]");
|
||||
expect(screen.getByTestId("modal-props-favorite-models").textContent).toBe("[]");
|
||||
expect(screen.getByTestId("modal-props-has-toggle-favorite").textContent).toBe("yes");
|
||||
expect(screen.getByTestId("modal-props-has-toggle-model-favorite").textContent).toBe("yes");
|
||||
});
|
||||
|
||||
it("selects dependencies and includes them in submit payload", async () => {
|
||||
const { props } = renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
Reference in New Issue
Block a user