feat(dashboard): collapse planning model + depth into advanced disclosure

Group the planning model selector and depth controls under a collapsible
"Advanced planning settings" disclosure (defaultOpen) in the Planning Mode
modal. Adds a defaultOpen prop to OnboardingDisclosure and tightens the
node.capabilities readonly typing in NodeDetailModal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-03 01:29:30 -07:00
parent 9619cd102d
commit adbc6130e9
6 changed files with 111 additions and 81 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Group planning model selector and depth controls under a collapsible "Advanced planning settings" disclosure in the Planning Mode modal.

View File

@@ -127,7 +127,10 @@ export function NodeDetailModal({
return getProjectsForNode(projects, node); return getProjectsForNode(projects, node);
}, [node, projects]); }, [node, projects]);
const isManagedDockerNode = useMemo(() => node?.capabilities?.includes("docker-managed") ?? false, [node]); const isManagedDockerNode = useMemo(
() => (node?.capabilities as readonly string[] | undefined)?.includes("docker-managed") ?? false,
[node],
);
const handleHealthCheck = useCallback(async () => { const handleHealthCheck = useCallback(async () => {
if (!node) return; if (!node) return;

View File

@@ -7,6 +7,7 @@ interface OnboardingDisclosureProps {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
onToggle?: (isOpen: boolean) => void; onToggle?: (isOpen: boolean) => void;
defaultOpen?: boolean;
} }
export function OnboardingDisclosure({ export function OnboardingDisclosure({
@@ -14,8 +15,9 @@ export function OnboardingDisclosure({
children, children,
className = "", className = "",
onToggle, onToggle,
defaultOpen = false,
}: OnboardingDisclosureProps) { }: OnboardingDisclosureProps) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(defaultOpen);
return ( return (
<div className={`onboarding-disclosure ${className}`.trim()}> <div className={`onboarding-disclosure ${className}`.trim()}>

View File

@@ -405,6 +405,18 @@
flex-shrink: 0; flex-shrink: 0;
} }
.planning-advanced-disclosure {
width: 100%;
max-width: 520px;
margin: 0 auto;
}
.planning-advanced-blurb {
margin: 0;
color: var(--text-muted);
font-size: 12px;
}
.planning-model-select-group { .planning-model-select-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -1556,91 +1556,97 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
</div> </div>
</div> </div>
<div className="planning-model-select-group"> <OnboardingDisclosure summary="Advanced planning settings" className="planning-advanced-disclosure" defaultOpen>
<label htmlFor="planning-modal-model" className="form-label"> <p className="planning-advanced-blurb">
Planning Model Choose the planning model and tune plan depth to control how detailed the AI interview should be.
{modelsLoading && ( </p>
<span className="text-muted text-muted-sm">
Loading models… <div className="planning-model-select-group">
</span> <label htmlFor="planning-modal-model" className="form-label">
Planning Model
{modelsLoading && (
<span className="text-muted text-muted-sm">
Loading models…
</span>
)}
</label>
<CustomModelDropdown
id="planning-modal-model"
label="Planning Model"
value={planningSelectionValue}
onChange={(value) => {
const { provider, modelId } = parseModelSelection(value);
setPlanningModelProvider(provider);
setPlanningModelId(modelId);
}}
models={loadedModels}
disabled={modelsLoading}
favoriteProviders={favoriteProviders}
onToggleFavorite={handleToggleFavoriteProvider}
favoriteModels={favoriteModels}
onToggleModelFavorite={handleToggleFavoriteModel}
/>
{modelsError && (
<div className="form-hint form-hint-error">
{modelsError}{" "}
<button
type="button"
className="text-link-btn"
onClick={() => {
void loadModels();
}}
>
Retry
</button>
</div>
)} )}
</label> <div className="model-selector-current model-selector-current--spaced">
<CustomModelDropdown <span
id="planning-modal-model" className={`model-badge ${
label="Planning Model" planningModelProvider && planningModelId
value={planningSelectionValue} ? "model-badge-custom"
onChange={(value) => { : "model-badge-default"
const { provider, modelId } = parseModelSelection(value); }`}
setPlanningModelProvider(provider);
setPlanningModelId(modelId);
}}
models={loadedModels}
disabled={modelsLoading}
favoriteProviders={favoriteProviders}
onToggleFavorite={handleToggleFavoriteProvider}
favoriteModels={favoriteModels}
onToggleModelFavorite={handleToggleFavoriteModel}
/>
{modelsError && (
<div className="form-hint form-hint-error">
{modelsError}{" "}
<button
type="button"
className="text-link-btn"
onClick={() => {
void loadModels();
}}
> >
Retry {getModelBadgeLabel(planningModelProvider, planningModelId)}
</button> </span>
</div> </div>
)}
<div className="model-selector-current model-selector-current--spaced">
<span
className={`model-badge ${
planningModelProvider && planningModelId
? "model-badge-custom"
: "model-badge-default"
}`}
>
{getModelBadgeLabel(planningModelProvider, planningModelId)}
</span>
</div> </div>
</div>
</div>
<div className="planning-depth-selector"> <div className="planning-depth-selector">
<div className="planning-depth-chip-group" role="group" aria-label="Planning depth"> <div className="planning-depth-chip-group" role="group" aria-label="Planning depth">
{([ {([
{ value: "small", label: "Small" }, { value: "small", label: "Small" },
{ value: "medium", label: "Medium" }, { value: "medium", label: "Medium" },
{ value: "large", label: "Large" }, { value: "large", label: "Large" },
] as const).map((depthOption) => ( ] as const).map((depthOption) => (
<button <button
key={depthOption.value} key={depthOption.value}
type="button" type="button"
className={`planning-depth-chip btn ${planningDepth === depthOption.value ? "btn-primary planning-depth-chip-active" : ""}`} className={`planning-depth-chip btn ${planningDepth === depthOption.value ? "btn-primary planning-depth-chip-active" : ""}`}
onClick={() => setPlanningDepth(depthOption.value)} onClick={() => setPlanningDepth(depthOption.value)}
aria-pressed={planningDepth === depthOption.value} aria-pressed={planningDepth === depthOption.value}
> >
{depthOption.label} {depthOption.label}
</button> </button>
))} ))}
</div> </div>
<label className="planning-depth-question-count" htmlFor="planning-depth-questions"> <label className="planning-depth-question-count" htmlFor="planning-depth-questions">
<span>Questions</span> <span>Questions</span>
<input <input
id="planning-depth-questions" id="planning-depth-questions"
className="input planning-depth-question-input" className="input planning-depth-question-input"
type="number" type="number"
min={1} min={1}
max={20} max={20}
value={customQuestionCount} value={customQuestionCount}
onChange={(e) => setCustomQuestionCount(e.target.value)} onChange={(e) => setCustomQuestionCount(e.target.value)}
placeholder="Auto" placeholder="Auto"
/> />
</label> </label>
</div>
</OnboardingDisclosure>
</div> </div>
<div className="planning-view-footer"> <div className="planning-view-footer">

View File

@@ -410,6 +410,8 @@ describe("PlanningModeModal", () => {
/> />
); );
expect(screen.getByRole("button", { name: "Advanced planning settings" })).toBeDefined();
expect(screen.getByText(/Choose the planning model and tune plan depth/)).toBeDefined();
expect(screen.getByRole("button", { name: "Small" })).toBeDefined(); expect(screen.getByRole("button", { name: "Small" })).toBeDefined();
expect(screen.getByRole("button", { name: "Medium" }).getAttribute("aria-pressed")).toBe("true"); expect(screen.getByRole("button", { name: "Medium" }).getAttribute("aria-pressed")).toBe("true");
expect(screen.getByRole("button", { name: "Large" })).toBeDefined(); expect(screen.getByRole("button", { name: "Large" })).toBeDefined();