diff --git a/.changeset/fix-loading-spinners.md b/.changeset/fix-loading-spinners.md new file mode 100644 index 0000000000..052fa47f8f --- /dev/null +++ b/.changeset/fix-loading-spinners.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Fix loading spinners that didn't spin across the dashboard. Many loading states (Settings, task tabs, agents, documents, plugins, model pickers, command center, and more) rendered bare "Loading…" text with no spinner — and a couple rendered an unstyled `loading-spinner` div that never showed anything. Added a shared `` component (self-contained animated SVG, no `lucide-react` dependency so it survives partial test mocks) and adopted it across ~45 loading placeholders so every loading state now shows a consistent animated spinner. diff --git a/packages/dashboard/app/components/AgentDetailView.tsx b/packages/dashboard/app/components/AgentDetailView.tsx index 1188d522a9..51935bcd0c 100644 --- a/packages/dashboard/app/components/AgentDetailView.tsx +++ b/packages/dashboard/app/components/AgentDetailView.tsx @@ -18,6 +18,7 @@ import type { Agent } from "../api"; import type { AgentLogEntry, Task, Message, ParticipantType, AgentPermissionPolicy, AgentPermissionPolicyRules } from "@fusion/core"; import { getErrorMessage, isEphemeralAgent } from "@fusion/core"; import { AgentLogViewer } from "./AgentLogViewer"; +import { LoadingSpinner } from "./LoadingSpinner"; import { AgentReflectionsTab } from "./AgentReflectionsTab"; import { getAgentHealthStatus } from "../utils/agentHealth"; import type { AgentHealthStatus } from "../utils/agentHealth"; @@ -4682,7 +4683,7 @@ function ConfigTab({
{runtimesLoading ? ( - {t("agents.loadingRuntimes", "Loading runtimes…")} + ) : ( e.preventDefault()}>
{t("newTaskModal.selectAgent", "Select agent")}
- {agentsLoading &&
{t("newTaskModal.loadingAgents", "Loading agents...")}
} + {agentsLoading &&
} {!agentsLoading && agents.map((a) => (
void; @@ -248,7 +249,7 @@ export function PiExtensionsManager({ addToast, projectId }: PiExtensionsManager
{loading ? ( -
{t("piManager.loading", "Loading Pi settings…")}
+
) : !settings ? (
@@ -434,7 +435,7 @@ export function PiExtensionsManager({ addToast, projectId }: PiExtensionsManager

{extensionsLoading ? ( -
{t("piManager.loadingExtensions", "Loading extensions…")}
+
) : extensions.length === 0 ? (
diff --git a/packages/dashboard/app/components/PluginManager.tsx b/packages/dashboard/app/components/PluginManager.tsx index 6ef6b7f4cb..97f2779d0e 100644 --- a/packages/dashboard/app/components/PluginManager.tsx +++ b/packages/dashboard/app/components/PluginManager.tsx @@ -16,6 +16,7 @@ import { useTranslation } from "react-i18next"; import { Package, Settings, Trash2, Plus, X, RefreshCw, RotateCcw, ExternalLink, Shield } from "lucide-react"; import { fetchPlugins, fetchPluginRegistry, installPlugin, enablePlugin, disablePlugin, uninstallPlugin, fetchPluginSettings, updatePluginSettings, reloadPlugin, fetchPluginSetupStatus, installPluginSetup, updatePlugin, rescanPlugin } from "../api"; import { DirectoryPicker } from "./DirectoryPicker"; +import { LoadingSpinner } from "./LoadingSpinner"; import type { PluginInstallation, PluginState, PluginSettingSchema } from "@fusion/core"; import type { PluginSetupStatusResponse, RegistryPluginEntry } from "../api"; import type { ToastType } from "../hooks/useToast"; @@ -726,7 +727,7 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
{t("plugins.settings", "Settings")}
{settingsLoading ? ( -

{t("plugins.loading", "Loading...")}

+

) : (() => { const effectiveSettingsSchema = resolveSettingsSchema(selectedPlugin); @@ -1198,7 +1199,7 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) { )} {loading ? ( -
{t("plugins.loadingPlugins", "Loading plugins...")}
+
) : ( <> {installedPlugins.length === 0 ? ( diff --git a/packages/dashboard/app/components/QuickChatFAB.tsx b/packages/dashboard/app/components/QuickChatFAB.tsx index 180ffd2434..37bbc3bc67 100644 --- a/packages/dashboard/app/components/QuickChatFAB.tsx +++ b/packages/dashboard/app/components/QuickChatFAB.tsx @@ -19,6 +19,7 @@ import { ChevronDown, Eye, EyeOff, Hash, MessageSquare, Paperclip, Pencil, Plus, import { attachmentBaseUrlForRoom, type Agent, type ModelInfo } from "../api"; import type { DiscoveredSkill } from "@fusion/dashboard"; import { CustomModelDropdown } from "./CustomModelDropdown"; +import { LoadingSpinner } from "./LoadingSpinner"; import { ChatQuestionResponse } from "./ChatQuestionResponse"; import { ProviderIcon } from "./ProviderIcon"; import { AgentMentionPopup } from "./AgentMentionPopup"; @@ -3205,7 +3206,7 @@ export function QuickChatFAB({
{sessionsLoading ? ( -
{t("chat.loadingConversation", "Loading conversation…")}
+
) : !roomThreadActive && isStreaming ? ( <> {displayedMessages.map((message: ChatMessageInfo, index) => ( @@ -3264,7 +3265,7 @@ export function QuickChatFAB({
) : roomThreadActive ? roomsState.messagesLoading ? ( -
{t("chat.loadingConversation", "Loading conversation…")}
+
) : displayedMessages.length === 0 && !helpMessageVisible ? (
{t("chat.noMessagesYet", "No messages yet. Start the conversation!")}
) : ( @@ -3290,7 +3291,7 @@ export function QuickChatFAB({ )} ) : messagesLoading ? ( -
{t("chat.loadingConversation", "Loading conversation…")}
+
) : displayedMessages.length === 0 && !streamingText && !streamingThinking && !isStreaming && !helpMessageVisible ? (
{t("chat.noMessagesYet", "No messages yet. Start the conversation!")}
) : ( @@ -3510,7 +3511,7 @@ export function QuickChatFAB({ {showSkillMenu && (
{skillsLoading ? ( -
{t("chat.loadingSkills", "Loading skills…")}
+
) : filteredSkills.length === 0 ? (
{skillFilter ? t("chat.noSkillsFound", "No skills found") : t("chat.noSkillsAvailable", "No skills available")} diff --git a/packages/dashboard/app/components/QuickEntryBox.tsx b/packages/dashboard/app/components/QuickEntryBox.tsx index 773e8e5920..fe56a04898 100644 --- a/packages/dashboard/app/components/QuickEntryBox.tsx +++ b/packages/dashboard/app/components/QuickEntryBox.tsx @@ -10,6 +10,7 @@ import { checkDuplicateTasks, fetchModels, fetchSettings, refineText, getRefineE import { DuplicateWarningModal } from "./DuplicateWarningModal"; import { Link, Paperclip, Brain, Lightbulb, ListTree, Sparkles, Save, ChevronDown, ChevronUp, ChevronRight, Bot, Server, Flag } from "lucide-react"; import { CustomModelDropdown } from "./CustomModelDropdown"; +import { LoadingSpinner } from "./LoadingSpinner"; import { getScopedItem, removeScopedItem, setScopedItem } from "../utils/projectStorage"; import { useNodes } from "../hooks/useNodes"; import { NodeHealthDot } from "./NodeHealthDot"; @@ -1992,7 +1993,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, }} >
{t("tasks.selectAgent", "Select agent")}
- {agentsLoading &&
{t("tasks.loadingAgents", "Loading agents...")}
} + {agentsLoading &&
} {!agentsLoading && agents.map((a) => (
- {loading &&

{t("research.loadingRuns", "Loading research runs…")}

} + {loading &&

} {!loading && error &&

{error}

} {!loading && !error && runs.length === 0 &&

{t("research.noRunsYet", "No research runs yet")}

}
diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx index 4a75bc073d..da3615aab9 100644 --- a/packages/dashboard/app/components/SettingsModal.tsx +++ b/packages/dashboard/app/components/SettingsModal.tsx @@ -38,6 +38,7 @@ import { AgentPermissionsSection } from "./settings/sections/AgentPermissionsSec import { MemorySection } from "./settings/sections/MemorySection"; import { ResearchProjectSection } from "./settings/sections/ResearchProjectSection"; import { BackupsSection } from "./settings/sections/BackupsSection"; +import { LoadingSpinner } from "./LoadingSpinner"; import { PluginsSection } from "./settings/sections/PluginsSection"; import { useMemoryBackendStatus } from "../hooks/useMemoryBackendStatus"; import { useOverlayDismiss } from "../hooks/useOverlayDismiss"; @@ -2910,7 +2911,7 @@ export function SettingsModal({
{loading ? ( -
{t("settings.loading", "Loading…")}
+
) : (
{showMobileSectionPicker && ( diff --git a/packages/dashboard/app/components/SettingsSyncLog.tsx b/packages/dashboard/app/components/SettingsSyncLog.tsx index aa5b1a5381..ca55fc0af2 100644 --- a/packages/dashboard/app/components/SettingsSyncLog.tsx +++ b/packages/dashboard/app/components/SettingsSyncLog.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { ChevronDown, Download, Upload } from "lucide-react"; import "./SettingsSyncLog.css"; import { linkifyFilePaths } from "../utils/filePathLinkify"; +import { LoadingSpinner } from "./LoadingSpinner"; // ── Types ───────────────────────────────────────────────────────────────────── @@ -168,7 +169,7 @@ export function SettingsSyncLog({
{loading && entries.length === 0 ? ( -
{t("syncLog.loading", "Loading...")}
+
) : filteredEntries.length === 0 ? (
{t("syncLog.noHistory", "No sync history available")}
) : ( diff --git a/packages/dashboard/app/components/SkillMultiselect.tsx b/packages/dashboard/app/components/SkillMultiselect.tsx index ec051d41f7..d6761b15c1 100644 --- a/packages/dashboard/app/components/SkillMultiselect.tsx +++ b/packages/dashboard/app/components/SkillMultiselect.tsx @@ -3,6 +3,7 @@ import { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { fetchDiscoveredSkills } from "../api"; import type { DiscoveredSkill } from "../api"; +import { LoadingSpinner } from "./LoadingSpinner"; export interface SkillMultiselectProps { /** Currently selected skill IDs */ @@ -116,7 +117,7 @@ export function SkillMultiselect({
{isLoading ? ( - {t("skills.loading", "Loading skills…")} + ) : availableSkills.length === 0 ? ( diff --git a/packages/dashboard/app/components/StashRecoveryView.tsx b/packages/dashboard/app/components/StashRecoveryView.tsx index fc92d2a7ed..3ddb3f36d8 100644 --- a/packages/dashboard/app/components/StashRecoveryView.tsx +++ b/packages/dashboard/app/components/StashRecoveryView.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { api } from "../api"; import { useConfirm } from "../hooks/useConfirm"; +import { LoadingSpinner } from "./LoadingSpinner"; import "./StashRecoveryView.css"; type RecordItem = { @@ -127,7 +128,7 @@ export function StashRecoveryView() { ×
- {diffState.loading &&

{t("stashRecovery.loadingDiff", "Loading diff…")}

} + {diffState.loading &&

} {diffState.error &&
{diffState.error}
} {!diffState.loading && !diffState.error && ( <> diff --git a/packages/dashboard/app/components/TaskChangesTab.tsx b/packages/dashboard/app/components/TaskChangesTab.tsx index 7241aca1ca..5b220116eb 100644 --- a/packages/dashboard/app/components/TaskChangesTab.tsx +++ b/packages/dashboard/app/components/TaskChangesTab.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { FileCode, ChevronDown, ChevronRight, ChevronLeft, AlertCircle, GitCommit, WrapText, Maximize2 } from "lucide-react"; import type { MergeDetails, ColumnId } from "@fusion/core"; +import { LoadingSpinner } from "./LoadingSpinner"; import { getErrorMessage } from "@fusion/core"; import { fetchTaskDiff, @@ -290,8 +291,7 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai return (
-
- {t("taskChanges.loading", "Loading changes...")} +
); diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index 25201ddcb4..90b7546338 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -48,6 +48,7 @@ import { TaskTokenStatsPanel } from "./TaskTokenStatsPanel"; import { BranchGroupCard } from "./BranchGroupCard"; import { PluginSlot } from "./PluginSlot"; import { ProviderIcon } from "./ProviderIcon"; +import { LoadingSpinner } from "./LoadingSpinner"; import { subscribeSse } from "../sse-bus"; import type { SessionTerminalMode, SessionTerminalPosture } from "./SessionTerminal"; import { usePluginUiSlots } from "../hooks/usePluginUiSlots"; @@ -3492,7 +3493,7 @@ export function TaskDetailContent({ ) : activeTab === "terminal" ? (
{cliSession && cliTabVisibility.kind !== "hidden" ? ( - {t("taskDetail.terminal.loading", "Loading terminal…")}
}> +
}> - {agentsLoading &&
{t("taskDetail.agent.loadingAgents", "Loading agents...")}
} + {agentsLoading &&
} {!agentsLoading && agents.map((a) => (
) : detailLoading ? ( -
{t("taskDetail.spec.loading", "Loading specification…")}
+
) : workingTask.prompt ? (
diff --git a/packages/dashboard/app/components/TaskDocumentsTab.tsx b/packages/dashboard/app/components/TaskDocumentsTab.tsx index 9127e2a5ff..be55ec98be 100644 --- a/packages/dashboard/app/components/TaskDocumentsTab.tsx +++ b/packages/dashboard/app/components/TaskDocumentsTab.tsx @@ -13,6 +13,7 @@ import { putTaskDocument, deleteTaskDocument, } from "../api"; +import { LoadingSpinner } from "./LoadingSpinner"; // Document key validation: alphanumeric, hyphens, underscores, 1-64 chars const DOCUMENT_KEY_REGEX = /^[a-zA-Z0-9_-]{1,64}$/; @@ -213,7 +214,7 @@ export function TaskDocumentsTab({ return (

{t("taskDocuments.heading", "Documents")}

-
{t("taskDocuments.loading", "Loading documents…")}
+
); } @@ -325,7 +326,7 @@ export function TaskDocumentsTab({
{t("taskDocuments.revisionHistory", "Revision History")}
{loadingRevisions ? ( -
{t("taskDocuments.loadingRevisions", "Loading…")}
+
) : revisions.length <= 1 ? (
{t("taskDocuments.noPreviousRevisions", "No previous revisions.")}
) : ( diff --git a/packages/dashboard/app/components/TaskForm.tsx b/packages/dashboard/app/components/TaskForm.tsx index 7733f73cf3..4112bfca78 100644 --- a/packages/dashboard/app/components/TaskForm.tsx +++ b/packages/dashboard/app/components/TaskForm.tsx @@ -6,6 +6,7 @@ import { fetchModels, fetchSettings, fetchWorkflows, refineText, getRefineErrorM import { applyPresetToSelection, getRecommendedPresetForSize } from "../utils/modelPresets"; import { CustomModelDropdown } from "./CustomModelDropdown"; import { NodeHealthDot } from "./NodeHealthDot"; +import { LoadingSpinner } from "./LoadingSpinner"; import { Sparkles, ChevronUp, ChevronDown, Maximize2, Minimize2 } from "lucide-react"; import { REPO_OVERRIDE_RE, resolveEffectiveGithubRepoDefault } from "./githubTracking"; @@ -1108,7 +1109,7 @@ export function TaskForm({
)} {modelsLoading ? ( -
{t("taskForm.loadingModels", "Loading models…")}
+
) : availableModels.length === 0 ? ( {t("taskForm.noModelsAvailable", "No models available. Configure authentication in Settings.")} ) : ( @@ -1292,7 +1293,7 @@ export function TaskForm({ {workflowsLoading ? (
- {t("taskForm.workflowsLoading", "Loading workflows…")} +
) : workflows.length === 0 ? ( // Built-ins are always present, so an empty list means the fetch diff --git a/packages/dashboard/app/components/TaskReviewTab.tsx b/packages/dashboard/app/components/TaskReviewTab.tsx index e4e20f5d78..6affdbbf83 100644 --- a/packages/dashboard/app/components/TaskReviewTab.tsx +++ b/packages/dashboard/app/components/TaskReviewTab.tsx @@ -11,6 +11,7 @@ import { fetchTaskReview, refreshTaskReview, reviseTaskReviewItems, updateTask } import type { SelectedReviewItem } from "../api"; import type { ToastType } from "../hooks/useToast"; import { linkifyFilePaths, linkifyReactChildren } from "../utils/filePathLinkify"; +import { LoadingSpinner } from "./LoadingSpinner"; interface Props { task: Task | TaskDetail; @@ -373,7 +374,7 @@ export function TaskReviewTab({ source: formatRefreshSource(review?.refreshSource, t), })}
- {loading ?
{t("taskReview.loadingData", "Loading review data…")}
: null} + {loading ?
: null} {!loading && error ?
{error}
: null} {!loading && !error && !isPrMode && displayItems.length === 0 ?
{emptyMessage ?? t("taskReview.noFeedbackDirect", "No reviewer feedback yet — this task has not produced reviewer-agent feedback in direct mode.")}
: null} {!loading && !error && displayItems.length > 0 ? ( diff --git a/packages/dashboard/app/components/TodoView.tsx b/packages/dashboard/app/components/TodoView.tsx index f7dfe6c3c2..4ed71c3e5a 100644 --- a/packages/dashboard/app/components/TodoView.tsx +++ b/packages/dashboard/app/components/TodoView.tsx @@ -19,6 +19,7 @@ import { createTask, fetchAgents } from "../api"; import type { Agent } from "../api"; import { useTodoLists } from "../hooks/useTodoLists"; import { useConfirm } from "../hooks/useConfirm"; +import { LoadingSpinner } from "./LoadingSpinner"; import "./TodoView.css"; interface TodoViewProps { @@ -684,7 +685,7 @@ export function TodoView({ }} > {agentsLoading ? ( -
{t("todo.loadingAgents", "Loading agents...")}
+
) : agents.length > 0 ? ( agents .map((agent) => ( diff --git a/packages/dashboard/app/components/WorkflowSettingsPanel.tsx b/packages/dashboard/app/components/WorkflowSettingsPanel.tsx index 36dc5522ee..7034aff014 100644 --- a/packages/dashboard/app/components/WorkflowSettingsPanel.tsx +++ b/packages/dashboard/app/components/WorkflowSettingsPanel.tsx @@ -58,6 +58,7 @@ import { } from "./settings"; import type { ToastType } from "../hooks/useToast"; import { CustomModelDropdown } from "./CustomModelDropdown"; +import { LoadingSpinner } from "./LoadingSpinner"; import "./WorkflowSettingsPanel.css"; interface WorkflowSettingsPanelProps { @@ -922,7 +923,7 @@ function ValuesTab({ {settings.length === 0 ? (

{loading - ? t("workflowSettings.loading", "Loading…") + ? : t("workflowSettings.noDeclarations", "This workflow declares no settings, so there are no values to edit.")}

) : ( diff --git a/packages/dashboard/app/components/__tests__/LoadingSpinner.test.tsx b/packages/dashboard/app/components/__tests__/LoadingSpinner.test.tsx new file mode 100644 index 0000000000..940f854174 --- /dev/null +++ b/packages/dashboard/app/components/__tests__/LoadingSpinner.test.tsx @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import { LoadingSpinner } from "../LoadingSpinner"; + +describe("LoadingSpinner", () => { + it("renders a lucide svg carrying the animate-spin utility (so it actually spins)", () => { + const { container } = render(); + + const svg = container.querySelector("svg"); + expect(svg).not.toBeNull(); + // The animation only runs if the svg has the global spin utility class. + expect(svg).toHaveClass("animate-spin"); + expect(svg).toHaveAttribute("fill", "none"); // lucide stroke-only loader + }); + + it("shows the provided label text alongside the spinner", () => { + render(); + expect(screen.getByText("Loading specification…")).toBeInTheDocument(); + }); + + it("exposes a live status region for assistive tech", () => { + render(); + const status = screen.getByRole("status"); + expect(status).toHaveAttribute("aria-live", "polite"); + expect(status).toHaveClass("loading-spinner"); + }); + + it("renders an icon-only spinner when no label is given", () => { + const { container } = render(); + expect(container.querySelector("svg")).toHaveClass("animate-spin"); + expect(container.querySelector(".loading-spinner__label")).toBeNull(); + }); + + it("merges a caller className onto the wrapper for layout slotting", () => { + render(); + const status = screen.getByRole("status"); + expect(status).toHaveClass("loading-spinner"); + expect(status).toHaveClass("spec-loading"); + }); +}); diff --git a/packages/dashboard/app/components/command-center/CommandCenter.tsx b/packages/dashboard/app/components/command-center/CommandCenter.tsx index 098a1d60da..14b655c95d 100644 --- a/packages/dashboard/app/components/command-center/CommandCenter.tsx +++ b/packages/dashboard/app/components/command-center/CommandCenter.tsx @@ -4,6 +4,7 @@ import { AlertCircle, Gauge } from "lucide-react"; import type { ActivityAnalytics, ColorTheme, LiveSnapshot, SignalsAnalytics, ThemeMode, TokenAnalytics, ToolAnalytics } from "@fusion/core"; import { api } from "../../api/legacy"; import { DateRangePicker, defaultPresets, rangeFromPreset, type DateRange } from "./DateRangePicker"; +import { LoadingSpinner } from "../LoadingSpinner"; import { TokensArea } from "./areas/TokensArea"; import { ToolsArea } from "./areas/ToolsArea"; import { ActivityArea } from "./areas/ActivityArea"; @@ -274,7 +275,7 @@ function OverviewTab({ {controlsSection}
-

{t("commandCenter.loading", "Loading command center...")}

+

{throughputSection}
diff --git a/packages/dashboard/app/components/command-center/areas/TeamArea.tsx b/packages/dashboard/app/components/command-center/areas/TeamArea.tsx index bda72dcdc8..d5d9f5f633 100644 --- a/packages/dashboard/app/components/command-center/areas/TeamArea.tsx +++ b/packages/dashboard/app/components/command-center/areas/TeamArea.tsx @@ -9,6 +9,7 @@ import type { CostResult, OrgTreeNode, TeamAgentSummary, TeamAnalytics } from "@ import { fetchExecutorStats, fetchOrgTree } from "../../../api/legacy"; import { useAppSettings } from "../../../hooks/useAppSettings"; import { AgentAvatar } from "../../AgentAvatar"; +import { LoadingSpinner } from "../../LoadingSpinner"; import type { DateRange } from "../DateRangePicker"; import { Bar, type BarDatum } from "../charts/Bar"; import { Sparkline } from "../charts/Sparkline"; @@ -292,7 +293,7 @@ export function TeamArea({ range, projectId }: { range: DateRange; projectId?: s
{orgTreeState.status === "loading" ? ( -

{t("commandCenter.controls.orgChart.loading", "Loading org chart…")}

+

) : orgTreeState.status === "error" ? (

{orgTreeState.error}

) : orgTreeState.data.length === 0 ? ( diff --git a/packages/dashboard/app/components/settings/sections/AuthenticationSection.tsx b/packages/dashboard/app/components/settings/sections/AuthenticationSection.tsx index 3936a6da22..60e6d218a9 100644 --- a/packages/dashboard/app/components/settings/sections/AuthenticationSection.tsx +++ b/packages/dashboard/app/components/settings/sections/AuthenticationSection.tsx @@ -8,6 +8,7 @@ import { LlamaCppProviderCard } from "../../LlamaCppProviderCard"; import { ProviderIcon } from "../../ProviderIcon"; import { PluginSlot } from "../../PluginSlot"; import { LoginInstructions } from "../../LoginInstructions"; +import { LoadingSpinner } from "../../LoadingSpinner"; import { OAuthManualCodeForm } from "../../OAuthManualCodeForm"; import { CustomProvidersSection } from "../../CustomProvidersSection"; import { copyTextToClipboard } from "../../../utils/copyToClipboard"; @@ -80,7 +81,7 @@ export function AuthenticationSection({ auth }: AuthenticationSectionProps) { (llamaCppProvider && !llamaCppProvider.authenticated); return (<>

{t("settings.auth.title", "Authentication")}

- {authLoading ? (
{t("settings.auth.loadingStatus", "Loading authentication status…")}
) : authProviders.length === 0 ? (
+ {authLoading ? (
) : authProviders.length === 0 ? (
{t("settings.auth.noProviders", "No providers available")}
) : (
{ void loadAuthStatus(); } }}/> diff --git a/packages/dashboard/app/components/settings/sections/BackupsSection.tsx b/packages/dashboard/app/components/settings/sections/BackupsSection.tsx index 9edbfdbf02..2f10c64874 100644 --- a/packages/dashboard/app/components/settings/sections/BackupsSection.tsx +++ b/packages/dashboard/app/components/settings/sections/BackupsSection.tsx @@ -2,6 +2,7 @@ import type { ReactNode } from "react"; import { useTranslation } from "react-i18next"; import type { BackupListResponse } from "../../../api"; import type { SectionBaseProps } from "./context"; +import { LoadingSpinner } from "../../LoadingSpinner"; export interface BackupsSectionProps extends SectionBaseProps { scopeBanner: ReactNode; backupInfo: BackupListResponse | null; @@ -75,7 +76,7 @@ export function BackupsSection({ scopeBanner, form, setForm, backupInfo, backupL
- {backupLoading ? (
{t("settings.backups.loadingBackupInfo", "Loading backup info\u2026")}
) : backupInfo ? (
+ {backupLoading ? (
) : backupInfo ? (
diff --git a/packages/dashboard/app/components/settings/sections/GlobalModelsSection.tsx b/packages/dashboard/app/components/settings/sections/GlobalModelsSection.tsx index 9ed772173c..13685fe9bf 100644 --- a/packages/dashboard/app/components/settings/sections/GlobalModelsSection.tsx +++ b/packages/dashboard/app/components/settings/sections/GlobalModelsSection.tsx @@ -5,6 +5,7 @@ import type { Settings, ThinkingLevel } from "@fusion/core"; import type { ModelInfo } from "../../../api"; import { CustomModelDropdown } from "../../CustomModelDropdown"; import type { SectionBaseProps, ModelLane } from "./context"; +import { LoadingSpinner } from "../../LoadingSpinner"; function toCommaSeparatedInput(values?: string[]): string { return values?.join(", ") ?? ""; } @@ -32,7 +33,7 @@ export function GlobalModelsSection({ scopeBanner, form, setForm, availableModel {/* --- Default Model --- */}

{t("settings.globalModels.defaultModel", "Default Model")}

- {modelsLoading ? (
{t("settings.models.loadingModels", "Loading available models…")}
) : availableModels.length === 0 ? (
+ {modelsLoading ? (
) : availableModels.length === 0 ? (
{t("settings.models.noModels", "No models available. Configure authentication first.")}
) : (<>
diff --git a/packages/dashboard/app/components/settings/sections/MemorySection.tsx b/packages/dashboard/app/components/settings/sections/MemorySection.tsx index 5a61f08914..bb95f708fd 100644 --- a/packages/dashboard/app/components/settings/sections/MemorySection.tsx +++ b/packages/dashboard/app/components/settings/sections/MemorySection.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import type { MemoryBackendCapabilities, MemoryBackendStatus, MemoryFileInfo, MemoryRetrievalTestResult, } from "../../../api"; import { FileEditor } from "../../FileEditor"; import type { SectionBaseProps } from "./context"; +import { LoadingSpinner } from "../../LoadingSpinner"; const MEMORY_FILE_OPTION_LABEL_MAX_CHARS = 72; function truncateMiddle(value: string, maxChars: number): string { if (value.length <= maxChars) { @@ -164,7 +165,7 @@ export function MemorySection({ scopeBanner, form, setForm, memory }: MemorySect {!isMemoryEnabled && (
{t("settings.memory.memoryIsCurrentlyDisabledYouCanViewThe", " Memory is currently disabled. You can view the file, but editing is read-only until memory is re-enabled. ")}
)} {isMemoryEnabled && backendStatusResolved && !isBackendWritable && (
{t("settings.memory.memoryIsConfiguredWithAReadOnlyBackend", " Memory is configured with a read-only backend. You can view the file, but saving is disabled. ")}
)} - {memoryLoading ? (
{t("settings.memory.loadingMemory", "Loading memory\u2026")}
) : (
+ {memoryLoading ? (
) : (