diff --git a/ui/web/src/components/shared/loading-skeleton.tsx b/ui/web/src/components/shared/loading-skeleton.tsx
index ed827831..32102d94 100644
--- a/ui/web/src/components/shared/loading-skeleton.tsx
+++ b/ui/web/src/components/shared/loading-skeleton.tsx
@@ -39,6 +39,35 @@ export function DetailSkeleton() {
);
}
+/** Skeleton matching detail page layout (header + tabs card) to prevent layout shift on load. */
+export function DetailPageSkeleton({ tabs = 4 }: { tabs?: number }) {
+ return (
+
+
+
+
+ {Array.from({ length: tabs }).map((_, i) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+ );
+}
+
/** Spinner that only appears after a delay (default 500ms) to avoid flicker on fast loads. */
export function DeferredSpinner({ delay = 500 }: { delay?: number }) {
const [show, setShow] = useState(false);
diff --git a/ui/web/src/hooks/use-pending-pairings-count.ts b/ui/web/src/hooks/use-pending-pairings-count.ts
index a1094d0e..474901e7 100644
--- a/ui/web/src/hooks/use-pending-pairings-count.ts
+++ b/ui/web/src/hooks/use-pending-pairings-count.ts
@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from "react";
import { useWs } from "./use-ws";
+import { useAuthStore } from "@/stores/use-auth-store";
import { useWsEvent } from "./use-ws-event";
import { Methods, Events } from "@/api/protocol";
import { toast } from "@/stores/use-toast-store";
@@ -11,10 +12,11 @@ interface Options {
export function usePendingPairingsCount({ showToast }: Options = {}) {
const ws = useWs();
+ const connected = useAuthStore((s) => s.connected);
const [pendingCount, setPendingCount] = useState(0);
const fetchCount = useCallback(async () => {
- if (!ws.isConnected) return;
+ if (!connected) return;
try {
const res = await ws.call<{ pending: { code: string }[] }>(
Methods.PAIRING_LIST,
@@ -23,7 +25,7 @@ export function usePendingPairingsCount({ showToast }: Options = {}) {
} catch {
// ignore
}
- }, [ws]);
+ }, [ws, connected]);
useEffect(() => {
fetchCount();
diff --git a/ui/web/src/pages/agents/hooks/use-agent-links.ts b/ui/web/src/pages/agents/hooks/use-agent-links.ts
index e1f379e6..6d93e36b 100644
--- a/ui/web/src/pages/agents/hooks/use-agent-links.ts
+++ b/ui/web/src/pages/agents/hooks/use-agent-links.ts
@@ -1,15 +1,17 @@
import { useState, useCallback } from "react";
import { useWs } from "@/hooks/use-ws";
+import { useAuthStore } from "@/stores/use-auth-store";
import { Methods } from "@/api/protocol";
import type { AgentLinkData, AgentLinkSettings } from "@/types/agent";
export function useAgentLinks(agentId: string) {
const ws = useWs();
+ const connected = useAuthStore((s) => s.connected);
const [links, setLinks] = useState([]);
- const [loading, setLoading] = useState(false);
+ const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
- if (!ws.isConnected) return;
+ if (!connected) return;
setLoading(true);
try {
const res = await ws.call<{ links: AgentLinkData[]; count: number }>(
@@ -22,7 +24,7 @@ export function useAgentLinks(agentId: string) {
} finally {
setLoading(false);
}
- }, [ws, agentId]);
+ }, [ws, agentId, connected]);
const createLink = useCallback(
async (params: {
diff --git a/ui/web/src/pages/agents/hooks/use-agents.ts b/ui/web/src/pages/agents/hooks/use-agents.ts
index 6f732da6..42c1df74 100644
--- a/ui/web/src/pages/agents/hooks/use-agents.ts
+++ b/ui/web/src/pages/agents/hooks/use-agents.ts
@@ -19,7 +19,7 @@ export function useAgents() {
const connected = useAuthStore((s) => s.connected);
const queryClient = useQueryClient();
- const { data: agents = [], isLoading: loading, error: queryError } = useQuery({
+ const { data: agents = [], isPending: loading, error: queryError } = useQuery({
queryKey: queryKeys.agents.all,
queryFn: async () => {
// Try HTTP first (returns full agent data, filtered by user access)
diff --git a/ui/web/src/pages/approvals/hooks/use-approvals.ts b/ui/web/src/pages/approvals/hooks/use-approvals.ts
index 66b9e041..5d6824d1 100644
--- a/ui/web/src/pages/approvals/hooks/use-approvals.ts
+++ b/ui/web/src/pages/approvals/hooks/use-approvals.ts
@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from "react";
import { useWs } from "@/hooks/use-ws";
+import { useAuthStore } from "@/stores/use-auth-store";
import { useWsEvent } from "@/hooks/use-ws-event";
import { Methods, Events } from "@/api/protocol";
import { toast } from "@/stores/use-toast-store";
@@ -13,12 +14,13 @@ export interface PendingApproval {
export function useApprovals() {
const ws = useWs();
+ const connected = useAuthStore((s) => s.connected);
const [pending, setPending] = useState([]);
- const [loading, setLoading] = useState(false);
+ const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const load = useCallback(async () => {
- if (!ws.isConnected) return;
+ if (!connected) return;
setLoading(true);
setError(null);
try {
@@ -29,7 +31,7 @@ export function useApprovals() {
} finally {
setLoading(false);
}
- }, [ws]);
+ }, [ws, connected]);
useEffect(() => {
load();
diff --git a/ui/web/src/pages/channels/channel-detail/channel-detail-page.tsx b/ui/web/src/pages/channels/channel-detail/channel-detail-page.tsx
index 4204ed04..ad73cf5b 100644
--- a/ui/web/src/pages/channels/channel-detail/channel-detail-page.tsx
+++ b/ui/web/src/pages/channels/channel-detail/channel-detail-page.tsx
@@ -10,7 +10,7 @@ import { ChannelCredentialsTab } from "./channel-credentials-tab";
import { ChannelConfigTab } from "./channel-config-tab";
import { ChannelGroupsTab } from "./channel-groups-tab";
import { ChannelWritersTab } from "./channel-writers-tab";
-import { DeferredSpinner } from "@/components/shared/loading-skeleton";
+import { DetailPageSkeleton } from "@/components/shared/loading-skeleton";
import { channelTypeLabels } from "../channels-status-view";
import { useChannels } from "../hooks/use-channels";
@@ -34,14 +34,7 @@ export function ChannelDetailPage({ instanceId, onBack }: ChannelDetailPageProps
const [activeTab, setActiveTab] = useState("general");
if (loading || !instance) {
- return (
-
- );
+ return ;
}
const status = channels[instance.name] ?? null;
diff --git a/ui/web/src/pages/channels/hooks/use-channels.ts b/ui/web/src/pages/channels/hooks/use-channels.ts
index 30679772..4336b05c 100644
--- a/ui/web/src/pages/channels/hooks/use-channels.ts
+++ b/ui/web/src/pages/channels/hooks/use-channels.ts
@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from "react";
import { useWs } from "@/hooks/use-ws";
+import { useAuthStore } from "@/stores/use-auth-store";
import { Methods } from "@/api/protocol";
export interface ChannelStatus {
@@ -9,12 +10,13 @@ export interface ChannelStatus {
export function useChannels() {
const ws = useWs();
+ const connected = useAuthStore((s) => s.connected);
const [channels, setChannels] = useState>({});
- const [loading, setLoading] = useState(false);
+ const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const load = useCallback(async () => {
- if (!ws.isConnected) return;
+ if (!connected) return;
setLoading(true);
setError(null);
try {
@@ -27,7 +29,7 @@ export function useChannels() {
} finally {
setLoading(false);
}
- }, [ws]);
+ }, [ws, connected]);
useEffect(() => {
load();
diff --git a/ui/web/src/pages/chat/hooks/use-chat-sessions.ts b/ui/web/src/pages/chat/hooks/use-chat-sessions.ts
index b379400e..de7e54a3 100644
--- a/ui/web/src/pages/chat/hooks/use-chat-sessions.ts
+++ b/ui/web/src/pages/chat/hooks/use-chat-sessions.ts
@@ -11,12 +11,13 @@ import { useAuthStore } from "@/stores/use-auth-store";
export function useChatSessions(agentId: string) {
const ws = useWs();
const userId = useAuthStore((s) => s.userId);
+ const connected = useAuthStore((s) => s.connected);
const [sessions, setSessions] = useState([]);
- const [loading, setLoading] = useState(false);
+ const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const loadSessions = useCallback(async () => {
- if (!ws.isConnected) return;
+ if (!connected) return;
setLoading(true);
setError(null);
try {
@@ -34,7 +35,7 @@ export function useChatSessions(agentId: string) {
} finally {
setLoading(false);
}
- }, [ws, agentId]);
+ }, [ws, agentId, connected]);
useEffect(() => {
loadSessions();
diff --git a/ui/web/src/pages/config/hooks/use-config.ts b/ui/web/src/pages/config/hooks/use-config.ts
index e729e69a..2ff35d75 100644
--- a/ui/web/src/pages/config/hooks/use-config.ts
+++ b/ui/web/src/pages/config/hooks/use-config.ts
@@ -1,6 +1,7 @@
import { useState, useCallback, useRef } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useWs } from "@/hooks/use-ws";
+import { useAuthStore } from "@/stores/use-auth-store";
import { Methods } from "@/api/protocol";
import { queryKeys } from "@/lib/query-keys";
@@ -12,20 +13,21 @@ interface ConfigData {
export function useConfig() {
const ws = useWs();
+ const connected = useAuthStore((s) => s.connected);
const queryClient = useQueryClient();
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
const hashRef = useRef("");
- const { data, isLoading: loading } = useQuery({
+ const { data, isPending: loading } = useQuery({
queryKey: queryKeys.config.all,
queryFn: async (): Promise => {
- if (!ws.isConnected) return { config: {}, hash: "", path: "" };
const res = await ws.call(Methods.CONFIG_GET);
hashRef.current = res.hash;
return res;
},
staleTime: 5 * 60_000,
+ enabled: connected,
});
const config = data?.config ?? null;
diff --git a/ui/web/src/pages/cron/hooks/use-cron.ts b/ui/web/src/pages/cron/hooks/use-cron.ts
index f609e4db..5e0069ef 100644
--- a/ui/web/src/pages/cron/hooks/use-cron.ts
+++ b/ui/web/src/pages/cron/hooks/use-cron.ts
@@ -1,6 +1,7 @@
import { useCallback } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useWs } from "@/hooks/use-ws";
+import { useAuthStore } from "@/stores/use-auth-store";
import { Methods } from "@/api/protocol";
import { queryKeys } from "@/lib/query-keys";
import { toast } from "@/stores/use-toast-store";
@@ -49,17 +50,18 @@ export interface CronRunLogEntry {
export function useCron() {
const ws = useWs();
+ const connected = useAuthStore((s) => s.connected);
const queryClient = useQueryClient();
- const { data: jobs = [], isLoading: loading } = useQuery({
+ const { data: jobs = [], isPending: loading } = useQuery({
queryKey: queryKeys.cron.all,
queryFn: async () => {
- if (!ws.isConnected) return [];
const res = await ws.call<{ jobs: CronJob[] }>(Methods.CRON_LIST, {
includeDisabled: true,
});
return res.jobs ?? [];
},
+ enabled: connected,
});
const invalidate = useCallback(
diff --git a/ui/web/src/pages/nodes/hooks/use-nodes.ts b/ui/web/src/pages/nodes/hooks/use-nodes.ts
index 48614ad9..e78e38d8 100644
--- a/ui/web/src/pages/nodes/hooks/use-nodes.ts
+++ b/ui/web/src/pages/nodes/hooks/use-nodes.ts
@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from "react";
import { useWs } from "@/hooks/use-ws";
+import { useAuthStore } from "@/stores/use-auth-store";
import { useWsEvent } from "@/hooks/use-ws-event";
import { Methods, Events } from "@/api/protocol";
@@ -23,12 +24,13 @@ export interface PairedDevice {
export function useNodes() {
const ws = useWs();
+ const connected = useAuthStore((s) => s.connected);
const [pendingPairings, setPendingPairings] = useState([]);
const [pairedDevices, setPairedDevices] = useState([]);
- const [loading, setLoading] = useState(false);
+ const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
- if (!ws.isConnected) return;
+ if (!connected) return;
setLoading(true);
try {
const res = await ws.call<{
@@ -42,7 +44,7 @@ export function useNodes() {
} finally {
setLoading(false);
}
- }, [ws]);
+ }, [ws, connected]);
useEffect(() => {
load();
diff --git a/ui/web/src/pages/sessions/hooks/use-sessions.ts b/ui/web/src/pages/sessions/hooks/use-sessions.ts
index b4a1813b..a0231dba 100644
--- a/ui/web/src/pages/sessions/hooks/use-sessions.ts
+++ b/ui/web/src/pages/sessions/hooks/use-sessions.ts
@@ -1,6 +1,7 @@
import { useCallback } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useWs } from "@/hooks/use-ws";
+import { useAuthStore } from "@/stores/use-auth-store";
import { Methods } from "@/api/protocol";
import { queryKeys } from "@/lib/query-keys";
import type { SessionInfo, SessionPreview, Message } from "@/types/session";
@@ -13,12 +14,13 @@ interface UseSessionsOptions {
export function useSessions(opts: UseSessionsOptions = {}) {
const ws = useWs();
+ const connected = useAuthStore((s) => s.connected);
const queryClient = useQueryClient();
const { agentFilter, limit, offset } = opts;
const queryKey = queryKeys.sessions.list({ agentFilter, limit, offset });
- const { data, isLoading: loading } = useQuery({
+ const { data, isPending: loading } = useQuery({
queryKey,
queryFn: async () => {
if (!ws.isConnected) return { sessions: [] as SessionInfo[], total: 0 };
@@ -30,6 +32,7 @@ export function useSessions(opts: UseSessionsOptions = {}) {
return { sessions: res.sessions ?? [], total: res.total ?? 0 };
},
placeholderData: (prev) => prev,
+ enabled: connected,
});
const sessions = data?.sessions ?? [];
diff --git a/ui/web/src/pages/skills/hooks/use-skills.ts b/ui/web/src/pages/skills/hooks/use-skills.ts
index 3719c07f..7a7e2913 100644
--- a/ui/web/src/pages/skills/hooks/use-skills.ts
+++ b/ui/web/src/pages/skills/hooks/use-skills.ts
@@ -1,6 +1,7 @@
import { useCallback } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useWs, useHttp } from "@/hooks/use-ws";
+import { useAuthStore } from "@/stores/use-auth-store";
import { Methods } from "@/api/protocol";
import { queryKeys } from "@/lib/query-keys";
import type { SkillInfo } from "@/types/skill";
@@ -10,16 +11,17 @@ export type { SkillInfo };
export function useSkills() {
const ws = useWs();
const http = useHttp();
+ const connected = useAuthStore((s) => s.connected);
const queryClient = useQueryClient();
- const { data: skills = [], isLoading: loading } = useQuery({
+ const { data: skills = [], isPending: loading } = useQuery({
queryKey: queryKeys.skills.all,
queryFn: async () => {
- if (!ws.isConnected) return [];
const res = await ws.call<{ skills: SkillInfo[] }>(Methods.SKILLS_LIST);
return res.skills ?? [];
},
staleTime: 60_000,
+ enabled: connected,
});
const invalidate = useCallback(
diff --git a/ui/web/src/pages/teams/hooks/use-teams.ts b/ui/web/src/pages/teams/hooks/use-teams.ts
index b4003daf..57520b36 100644
--- a/ui/web/src/pages/teams/hooks/use-teams.ts
+++ b/ui/web/src/pages/teams/hooks/use-teams.ts
@@ -1,15 +1,17 @@
import { useState, useCallback } from "react";
import { useWs } from "@/hooks/use-ws";
+import { useAuthStore } from "@/stores/use-auth-store";
import { Methods } from "@/api/protocol";
import type { TeamData, TeamMemberData, TeamTaskData, TeamAccessSettings } from "@/types/team";
export function useTeams() {
const ws = useWs();
+ const connected = useAuthStore((s) => s.connected);
const [teams, setTeams] = useState([]);
- const [loading, setLoading] = useState(false);
+ const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
- if (!ws.isConnected) return;
+ if (!connected) return;
setLoading(true);
try {
const res = await ws.call<{ teams: TeamData[]; count: number }>(
@@ -21,7 +23,7 @@ export function useTeams() {
} finally {
setLoading(false);
}
- }, [ws]);
+ }, [ws, connected]);
const createTeam = useCallback(
async (params: {
diff --git a/ui/web/src/pages/tts/hooks/use-tts-config.ts b/ui/web/src/pages/tts/hooks/use-tts-config.ts
index 75373cba..6c532ea4 100644
--- a/ui/web/src/pages/tts/hooks/use-tts-config.ts
+++ b/ui/web/src/pages/tts/hooks/use-tts-config.ts
@@ -1,6 +1,7 @@
import { useState, useCallback } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useWs } from "@/hooks/use-ws";
+import { useAuthStore } from "@/stores/use-auth-store";
import { Methods } from "@/api/protocol";
import { queryKeys } from "@/lib/query-keys";
@@ -43,18 +44,19 @@ const DEFAULT_TTS: TtsConfig = {
export function useTtsConfig() {
const ws = useWs();
+ const connected = useAuthStore((s) => s.connected);
const queryClient = useQueryClient();
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
- const { data: tts = DEFAULT_TTS, isLoading: loading } = useQuery({
+ const { data: tts = DEFAULT_TTS, isPending: loading } = useQuery({
queryKey: queryKeys.tts.all,
queryFn: async () => {
- if (!ws.isConnected) return DEFAULT_TTS;
const res = await ws.call<{ config: Record }>(Methods.CONFIG_GET);
const ttsConfig = (res.config?.tts as TtsConfig) ?? DEFAULT_TTS;
return { ...DEFAULT_TTS, ...ttsConfig };
},
+ enabled: connected,
});
const invalidate = useCallback(
diff --git a/ui/web/src/pages/usage/hooks/use-usage.ts b/ui/web/src/pages/usage/hooks/use-usage.ts
index d562912f..950b2b06 100644
--- a/ui/web/src/pages/usage/hooks/use-usage.ts
+++ b/ui/web/src/pages/usage/hooks/use-usage.ts
@@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from "react";
import { useWs } from "@/hooks/use-ws";
+import { useAuthStore } from "@/stores/use-auth-store";
import { Methods } from "@/api/protocol";
export interface UsageRecord {
@@ -28,14 +29,15 @@ export interface UsageSummary {
export function useUsage() {
const ws = useWs();
+ const connected = useAuthStore((s) => s.connected);
const [records, setRecords] = useState([]);
const [total, setTotal] = useState(0);
const [summary, setSummary] = useState(null);
- const [loading, setLoading] = useState(false);
+ const [loading, setLoading] = useState(true);
const loadRecords = useCallback(
async (opts?: { agentId?: string; limit?: number; offset?: number }) => {
- if (!ws.isConnected) return;
+ if (!connected) return;
setLoading(true);
try {
const res = await ws.call<{ records: UsageRecord[]; total?: number }>(Methods.USAGE_GET, {
@@ -51,18 +53,18 @@ export function useUsage() {
setLoading(false);
}
},
- [ws],
+ [ws, connected],
);
const loadSummary = useCallback(async () => {
- if (!ws.isConnected) return;
+ if (!connected) return;
try {
const res = await ws.call(Methods.USAGE_SUMMARY);
setSummary(res);
} catch {
// ignore
}
- }, [ws]);
+ }, [ws, connected]);
useEffect(() => {
loadRecords();