FN-6101: pass planning model overrides to quick task workflows

Ensure quick task model overrides flow through planning and submission.

- add planning model selection state to inline task creation and include it in submit payloads only when explicitly overridden
- extend the model selection modal with an optional planning-model picker that clears preset mode consistently with other manual overrides
- cover planning, combined overrides, and default omission behavior in inline create card and quick entry tests

Files changed:
 packages/dashboard/app/components/InlineCreateCard.tsx  | 26 ++++++++-
 packages/dashboard/app/components/ModelSelectionModal.tsx         | 45 +++++++++++++++
 packages/dashboard/app/components/__tests__/InlineCreateCard.test.tsx | 67 +++++++++++++++++++++-
 packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx    | 49 ++++++++++++----
 4 files changed, 172 insertions(+), 15 deletions(-)

Fusion-Task-Id: FN-6101

Fusion-Task-Lineage: 7c1553d2-3edb-4b23-ad36-77756bbfe1dc
This commit is contained in:
gsxdsm
2026-06-09 11:00:03 -07:00
parent fc33a425fc
commit 7a2864f411
4 changed files with 174 additions and 17 deletions

View File

@@ -108,6 +108,8 @@ export function InlineCreateCard({
const [executorModelId, setExecutorModelId] = useState<string | undefined>(undefined);
const [validatorProvider, setValidatorProvider] = useState<string | undefined>(undefined);
const [validatorModelId, setValidatorModelId] = useState<string | undefined>(undefined);
const [planningProvider, setPlanningProvider] = useState<string | undefined>(undefined);
const [planningModelId, setPlanningModelId] = useState<string | undefined>(undefined);
const [browserVerification, setBrowserVerification] = useState(false);
const [priority, setPriority] = useState<TaskPriority>(DEFAULT_TASK_PRIORITY);
const [modelsLoading, setModelsLoading] = useState(false);
@@ -254,12 +256,14 @@ export function InlineCreateCard({
const executorSelectionValue = getModelSelectionValue(executorProvider, executorModelId);
const validatorSelectionValue = getModelSelectionValue(validatorProvider, validatorModelId);
const planningSelectionValue = getModelSelectionValue(planningProvider, planningModelId);
const availablePresets = settings?.modelPresets || [];
const selectedPreset = availablePresets.find((preset) => preset.id === selectedPresetId);
const hasExecutorOverride = Boolean(executorProvider && executorModelId);
const hasValidatorOverride = Boolean(validatorProvider && validatorModelId);
const selectedModelCount = Number(hasExecutorOverride) + Number(hasValidatorOverride);
const hasPlanningOverride = Boolean(planningProvider && planningModelId);
const selectedModelCount = Number(hasExecutorOverride) + Number(hasValidatorOverride) + Number(hasPlanningOverride);
// Track focus-out for conditional cancel behavior and justResetRef cleanup.
useEffect(() => {
@@ -383,6 +387,8 @@ export function InlineCreateCard({
setExecutorModelId(undefined);
setValidatorProvider(undefined);
setValidatorModelId(undefined);
setPlanningProvider(undefined);
setPlanningModelId(undefined);
setBrowserVerification(false);
setPriority(DEFAULT_TASK_PRIORITY);
setDependencies([]);
@@ -434,6 +440,8 @@ export function InlineCreateCard({
modelId: hasExecutorOverride ? executorModelId : undefined,
validatorModelProvider: hasValidatorOverride ? validatorProvider : undefined,
validatorModelId: hasValidatorOverride ? validatorModelId : undefined,
planningModelProvider: hasPlanningOverride ? planningProvider : undefined,
planningModelId: hasPlanningOverride ? planningModelId : undefined,
enabledWorkflowSteps: browserVerification ? ["browser-verification"] : undefined,
priority,
nodeId,
@@ -451,7 +459,7 @@ export function InlineCreateCard({
}
await submitTask(input);
}, [description, submitting, dependencies, selectedAgentId, selectedPresetId, hasExecutorOverride, executorProvider, executorModelId, hasValidatorOverride, validatorProvider, validatorModelId, browserVerification, priority, nodeId, projectId, addToast, submitTask]);
}, [description, submitting, dependencies, selectedAgentId, selectedPresetId, hasExecutorOverride, executorProvider, executorModelId, hasValidatorOverride, validatorProvider, validatorModelId, hasPlanningOverride, planningProvider, planningModelId, browserVerification, priority, nodeId, projectId, addToast, submitTask]);
const handleDuplicateProceed = useCallback(async () => {
const matches = duplicateMatches;
@@ -595,6 +603,12 @@ export function InlineCreateCard({
setValidatorModelId(next.modelId);
}, []);
const handlePlanningModelChange = useCallback((value: string) => {
const next = parseModelSelection(value);
setPlanningProvider(next.provider);
setPlanningModelId(next.modelId);
}, []);
const handleToggleFavorite = useCallback(async (provider: string) => {
const currentFavorites = favoriteProviders;
const isFavorite = currentFavorites.includes(provider);
@@ -655,6 +669,8 @@ export function InlineCreateCard({
setExecutorModelId(undefined);
setValidatorProvider(undefined);
setValidatorModelId(undefined);
setPlanningProvider(undefined);
setPlanningModelId(undefined);
setBrowserVerification(false);
setSelectedPresetId(undefined);
setSelectedAgentId(null);
@@ -681,6 +697,8 @@ export function InlineCreateCard({
setExecutorModelId(undefined);
setValidatorProvider(undefined);
setValidatorModelId(undefined);
setPlanningProvider(undefined);
setPlanningModelId(undefined);
setBrowserVerification(false);
setSelectedPresetId(undefined);
setSelectedAgentId(null);
@@ -1088,6 +1106,8 @@ export function InlineCreateCard({
setExecutorModelId(undefined);
setValidatorProvider(undefined);
setValidatorModelId(undefined);
setPlanningProvider(undefined);
setPlanningModelId(undefined);
setShowPresets(false);
}}
>
@@ -1163,8 +1183,10 @@ export function InlineCreateCard({
models={loadedModels}
executorValue={executorSelectionValue}
validatorValue={validatorSelectionValue}
planningValue={planningSelectionValue}
onExecutorChange={handleExecutorChange}
onValidatorChange={handleValidatorChange}
onPlanningChange={handlePlanningModelChange}
modelsLoading={modelsLoading}
modelsError={modelsError}
onRetry={loadModels}

View File

@@ -13,8 +13,10 @@ interface ModelSelectionModalProps {
models: ModelInfo[];
executorValue: string;
validatorValue: string;
planningValue?: string;
onExecutorChange: (value: string) => void;
onValidatorChange: (value: string) => void;
onPlanningChange?: (value: string) => void;
modelsLoading: boolean;
modelsError: string | null;
onRetry: () => void;
@@ -46,8 +48,10 @@ export function ModelSelectionModal({
models,
executorValue,
validatorValue,
planningValue = "",
onExecutorChange,
onValidatorChange,
onPlanningChange,
modelsLoading,
modelsError,
onRetry,
@@ -95,6 +99,7 @@ export function ModelSelectionModal({
onPresetChange(undefined);
onExecutorChange("");
onValidatorChange("");
onPlanningChange?.("");
return;
}
if (value === "custom") {
@@ -134,10 +139,22 @@ export function ModelSelectionModal({
[onPresetChange, selectedPresetId, onValidatorChange],
);
const handlePlanningChange = useCallback(
(value: string) => {
// Manual model selection clears preset mode
if (onPresetChange && selectedPresetId) {
onPresetChange(undefined);
}
onPlanningChange?.(value);
},
[onPresetChange, selectedPresetId, onPlanningChange],
);
if (!isOpen) return null;
const hasExecutorOverride = Boolean(executorValue);
const hasValidatorOverride = Boolean(validatorValue);
const hasPlanningOverride = Boolean(planningValue);
return (
<div className="modal-overlay open" onClick={handleOverlayClick} role="dialog" aria-modal="true" data-testid="model-selection-modal">
@@ -211,6 +228,34 @@ export function ModelSelectionModal({
</div>
)}
{onPlanningChange ? (
<div className="task-detail-section">
<div className="inline-create-model-row">
<label htmlFor="model-selection-planning" className="inline-create-model-label">
{t("modelSelection.planningModel", "Planning Model")}
</label>
<span
className={`model-badge ${hasPlanningOverride ? "model-badge-custom" : "model-badge-default"}`}
data-testid="planning-badge"
>
{getModelBadgeLabel(models, planningValue, t)}
</span>
<CustomModelDropdown
id="model-selection-planning"
label={t("modelSelection.planningModel", "Planning Model")}
value={planningValue}
onChange={handlePlanningChange}
models={models}
placeholder={t("modelSelection.planningPlaceholder", "Select planning model…")}
favoriteProviders={favoriteProviders}
onToggleFavorite={onToggleFavorite}
favoriteModels={favoriteModels}
onToggleModelFavorite={onToggleModelFavorite}
/>
</div>
</div>
) : null}
<div className="task-detail-section">
<div className="inline-create-model-row">
<label htmlFor="model-selection-executor" className="inline-create-model-label">

View File

@@ -37,8 +37,10 @@ vi.mock("../ModelSelectionModal", () => ({
models,
executorValue,
validatorValue,
planningValue,
onExecutorChange,
onValidatorChange,
onPlanningChange,
modelsLoading,
modelsError,
onRetry,
@@ -53,8 +55,10 @@ vi.mock("../ModelSelectionModal", () => ({
models: typeof MOCK_MODELS;
executorValue: string;
validatorValue: string;
planningValue: string;
onExecutorChange: (value: string) => void;
onValidatorChange: (value: string) => void;
onPlanningChange: (value: string) => void;
modelsLoading: boolean;
modelsError: string | null;
onRetry: () => void;
@@ -69,6 +73,7 @@ vi.mock("../ModelSelectionModal", () => ({
<div data-testid="model-selection-modal">
<div data-testid="modal-props-executor-value">{executorValue}</div>
<div data-testid="modal-props-validator-value">{validatorValue}</div>
<div data-testid="modal-props-planning-value">{planningValue}</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-models">{JSON.stringify(favoriteModels ?? [])}</div>
@@ -79,8 +84,10 @@ vi.mock("../ModelSelectionModal", () => ({
<button data-testid="modal-close" onClick={onClose}>Close</button>
<button data-testid="modal-select-executor" onClick={() => onExecutorChange("anthropic/claude-sonnet-4-5")}>Select Executor</button>
<button data-testid="modal-select-validator" onClick={() => onValidatorChange("openai/gpt-4o")}>Select Reviewer</button>
<button data-testid="modal-select-planning" onClick={() => onPlanningChange("anthropic/claude-sonnet-4-5")}>Select Planning</button>
<button data-testid="modal-clear-executor" onClick={() => onExecutorChange("")}>Clear Executor</button>
<button data-testid="modal-clear-validator" onClick={() => onValidatorChange("")}>Clear Reviewer</button>
<button data-testid="modal-clear-planning" onClick={() => onPlanningChange("")}>Clear Planning</button>
<button data-testid="modal-retry" onClick={onRetry}>Retry</button>
</div>
);
@@ -456,6 +463,29 @@ describe("InlineCreateCard model selector", () => {
expect(screen.getByTestId("modal-props-executor-value").textContent).toBe("");
});
it("omits model fields from the submit payload when no overrides are selected", async () => {
const { props } = renderCard();
expandCard();
const textarea = screen.getByPlaceholderText("What needs to be done?");
fireEvent.change(textarea, { target: { value: "Task using model defaults" } });
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
expect(props.onSubmit).toHaveBeenCalledWith(
expect.objectContaining({
description: "Task using model defaults",
modelProvider: undefined,
modelId: undefined,
validatorModelProvider: undefined,
validatorModelId: undefined,
planningModelProvider: undefined,
planningModelId: undefined,
}),
);
});
});
it("omits model fields from the submit payload after clearing back to default", async () => {
const { props } = renderCard();
expandCard();
@@ -477,6 +507,8 @@ describe("InlineCreateCard model selector", () => {
modelId: undefined,
validatorModelProvider: undefined,
validatorModelId: undefined,
planningModelProvider: undefined,
planningModelId: undefined,
}),
);
});
@@ -493,26 +525,55 @@ describe("InlineCreateCard model selector", () => {
expect(saveButton.className).toContain("btn-task-create");
});
it("includes selected models in the submit payload", async () => {
it("includes the selected planning model in the submit payload", async () => {
const { props } = renderCard();
expandCard();
const textarea = screen.getByPlaceholderText("What needs to be done?");
fireEvent.change(textarea, { target: { value: "Task with model overrides" } });
fireEvent.change(textarea, { target: { value: "Task with planning model override" } });
openModelModal();
fireEvent.click(screen.getByTestId("modal-select-executor"));
fireEvent.click(screen.getByTestId("modal-select-validator"));
fireEvent.click(screen.getByTestId("modal-select-planning"));
fireEvent.click(screen.getByTestId("modal-close"));
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
expect(props.onSubmit).toHaveBeenCalledWith(
expect.objectContaining({
description: "Task with model overrides",
description: "Task with planning model override",
modelProvider: undefined,
modelId: undefined,
validatorModelProvider: undefined,
validatorModelId: undefined,
planningModelProvider: "anthropic",
planningModelId: "claude-sonnet-4-5",
}),
);
});
});
it("includes all selected model pairs in the submit payload", async () => {
const { props } = renderCard();
expandCard();
const textarea = screen.getByPlaceholderText("What needs to be done?");
fireEvent.change(textarea, { target: { value: "Task with all model overrides" } });
openModelModal();
fireEvent.click(screen.getByTestId("modal-select-executor"));
fireEvent.click(screen.getByTestId("modal-select-validator"));
fireEvent.click(screen.getByTestId("modal-select-planning"));
fireEvent.click(screen.getByTestId("modal-close"));
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
expect(props.onSubmit).toHaveBeenCalledWith(
expect.objectContaining({
description: "Task with all model overrides",
modelProvider: "anthropic",
modelId: "claude-sonnet-4-5",
validatorModelProvider: "openai",
validatorModelId: "gpt-4o",
planningModelProvider: "anthropic",
planningModelId: "claude-sonnet-4-5",
}),
);
});

View File

@@ -1575,35 +1575,64 @@ describe("QuickEntryBox", () => {
}
});
it("includes selected models in submit payload", async () => {
it("includes all three selected model pairs in submit payload", async () => {
const { props } = renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Task with model" } });
fireEvent.change(textarea, { target: { value: "Task with model overrides" } });
openModelMenu();
// Menu should be open
expect(screen.getByTestId("model-nested-menu")).toBeTruthy();
fireEvent.click(screen.getByTestId("model-menu-plan"));
fireEvent.click(screen.getByTestId("dropdown-select-plan model"));
fireEvent.click(screen.getByTestId("model-submenu-back"));
// Navigate to executor submenu
fireEvent.click(screen.getByTestId("model-menu-executor"));
// Select executor model via mocked dropdown
fireEvent.click(screen.getByTestId("dropdown-select-executor model"));
fireEvent.click(screen.getByTestId("model-submenu-back"));
// Close the menu via Escape
fireEvent.click(screen.getByTestId("model-menu-validator"));
fireEvent.click(screen.getByTestId("dropdown-select-validator model"));
// Close the submenu, then close the menu before submitting.
fireEvent.keyDown(textarea, { key: "Escape" });
fireEvent.keyDown(textarea, { key: "Escape" });
// Submit the task
fireEvent.keyDown(textarea, { key: "Enter" });
await waitFor(() => {
expect(props.onCreate).toHaveBeenCalledWith(
expect.objectContaining({
description: "Task with model",
description: "Task with model overrides",
modelProvider: "anthropic",
modelId: "claude-sonnet-4-5",
validatorModelProvider: "anthropic",
validatorModelId: "claude-sonnet-4-5",
planningModelProvider: "anthropic",
planningModelId: "claude-sonnet-4-5",
}),
);
});
});
it("omits model fields from submit payload when no overrides are selected", async () => {
const { props } = renderQuickEntryBox({});
expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input");
fireEvent.change(textarea, { target: { value: "Task without model overrides" } });
fireEvent.keyDown(textarea, { key: "Enter" });
await waitFor(() => {
expect(props.onCreate).toHaveBeenCalledWith(
expect.objectContaining({
description: "Task without model overrides",
modelProvider: undefined,
modelId: undefined,
validatorModelProvider: undefined,
validatorModelId: undefined,
planningModelProvider: undefined,
planningModelId: undefined,
}),
);
});