From 3799d103a20ba6f942ac81399fe879b55e592083 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 2 Apr 2026 23:00:58 -0700 Subject: [PATCH] feat(FN-769): lift and wire favorite model state into ListView - Add favoriteProviders and favoriteModels state to AppInner - Initialize favorites from fetchModels response - Add handleToggleFavorite and handleToggleModelFavorite callbacks with optimistic updates and rollback on error - Pass favorite state and toggle handlers down to ListView component - Import updateGlobalSettings API for persisting favorite selections --- packages/dashboard/app/App.tsx | 49 ++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index f1b3e5a56..6219db5aa 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -1,6 +1,6 @@ import { useState, useCallback, useEffect } from "react"; import type { TaskDetail, TaskCreateInput, Task, ThemeMode } from "@fusion/core"; -import { fetchConfig, fetchSettings, fetchAuthStatus, updateSettings, fetchModels, fetchTaskDetail, updateProject, unregisterProject } from "./api"; +import { fetchConfig, fetchSettings, fetchAuthStatus, updateSettings, updateGlobalSettings, fetchModels, fetchTaskDetail, updateProject, unregisterProject } from "./api"; import type { ModelInfo, ProjectInfo } from "./api"; import { Header } from "./components/Header"; import { Board } from "./components/Board"; @@ -103,6 +103,8 @@ function AppInner() { const [searchQuery, setSearchQuery] = useState(""); const [githubTokenConfigured, setGithubTokenConfigured] = useState(false); const [availableModels, setAvailableModels] = useState([]); + const [favoriteProviders, setFavoriteProviders] = useState([]); + const [favoriteModels, setFavoriteModels] = useState([]); // Persist view mode useEffect(() => { @@ -180,10 +182,49 @@ function AppInner() { // Fetch available models useEffect(() => { fetchModels() - .then((response) => setAvailableModels(response.models)) + .then((response) => { + setAvailableModels(response.models); + setFavoriteProviders(response.favoriteProviders); + setFavoriteModels(response.favoriteModels); + }) .catch(() => {/* keep empty array on failure */}); }, []); + // Favorite provider/model toggle handlers (shared by ListView bulk edit) + const handleToggleFavorite = useCallback(async (provider: string) => { + const currentFavorites = favoriteProviders; + const isFavorite = currentFavorites.includes(provider); + const newFavorites = isFavorite + ? currentFavorites.filter((p) => p !== provider) + : [provider, ...currentFavorites]; + + setFavoriteProviders(newFavorites); + + try { + await updateGlobalSettings({ favoriteProviders: newFavorites, favoriteModels }); + } catch { + setFavoriteProviders(currentFavorites); + addToast("Failed to update favorites", "error"); + } + }, [favoriteProviders, favoriteModels, addToast]); + + const handleToggleModelFavorite = useCallback(async (modelId: string) => { + const currentFavorites = favoriteModels; + const isFavorite = currentFavorites.includes(modelId); + const newFavorites = isFavorite + ? currentFavorites.filter((m) => m !== modelId) + : [modelId, ...currentFavorites]; + + setFavoriteModels(newFavorites); + + try { + await updateGlobalSettings({ favoriteProviders, favoriteModels: newFavorites }); + } catch { + setFavoriteModels(currentFavorites); + addToast("Failed to update model favorites", "error"); + } + }, [favoriteModels, favoriteProviders, addToast]); + // Handle deep link to task on mount (with optional project context) useEffect(() => { const params = new URLSearchParams(window.location.search); @@ -491,6 +532,10 @@ function AppInner() { onPlanningMode={handleNewTaskPlanningMode} onSubtaskBreakdown={handleSubtaskBreakdown} availableModels={availableModels} + favoriteProviders={favoriteProviders} + favoriteModels={favoriteModels} + onToggleFavorite={handleToggleFavorite} + onToggleModelFavorite={handleToggleModelFavorite} /> ); };