From 2f32c9de750868400218ea649c335785baab9664 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 20 May 2026 13:44:00 -0700 Subject: [PATCH 1/3] Make Electric-backed UI cache-first --- .../useWorkspaceHostUrl.ts | 2 +- .../src/renderer/hooks/useCurrentPlan.ts | 5 +- .../automations/$automationId/page.tsx | 3 +- .../_dashboard/automations/page.tsx | 4 +- .../ResourceConsumption.tsx | 85 +++++++------------ .../resourceConsumptionPolicy.test.ts | 7 +- .../resourceConsumptionPolicy.ts | 4 +- .../useRemoteHostStatus.ts | 2 +- .../_dashboard/v2-workspace/layout.tsx | 2 +- .../useDefaultV2TerminalPresets.ts | 10 ++- .../AccountSettings/AccountSettings.tsx | 4 +- .../ApiKeysSettings/ApiKeysSettings.tsx | 7 +- .../components/HostSettings/HostSettings.tsx | 3 +- .../_authenticated/settings/hosts/page.tsx | 3 +- .../MembersSettings/MembersSettings.tsx | 4 +- .../PendingInvitations/PendingInvitations.tsx | 4 +- .../OrganizationSettings.tsx | 16 +++- .../_authenticated/settings/projects/page.tsx | 5 +- .../TeamDetailSettings/TeamDetailSettings.tsx | 8 +- .../TeamsSettings/TeamsSettings.tsx | 4 +- .../V2ProjectSettings/V2ProjectSettings.tsx | 11 ++- 21 files changed, 97 insertions(+), 96 deletions(-) diff --git a/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/useWorkspaceHostUrl.ts b/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/useWorkspaceHostUrl.ts index b8c59821c34..eb33228cc51 100644 --- a/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/useWorkspaceHostUrl.ts +++ b/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/useWorkspaceHostUrl.ts @@ -42,7 +42,7 @@ export function useWorkspaceHostTarget( const match = workspaceId ? (workspaceRows[0] ?? null) : null; return useMemo(() => { - if (!workspaceId || !isReady) return { status: "loading" }; + if (!workspaceId || (!isReady && !match)) return { status: "loading" }; if (!match) return { status: "not-found" }; if (!match.isSynced) return { status: "loading" }; if (machineId && match.hostId === machineId) { diff --git a/apps/desktop/src/renderer/hooks/useCurrentPlan.ts b/apps/desktop/src/renderer/hooks/useCurrentPlan.ts index 41d9243223c..753c19c23c7 100644 --- a/apps/desktop/src/renderer/hooks/useCurrentPlan.ts +++ b/apps/desktop/src/renderer/hooks/useCurrentPlan.ts @@ -50,12 +50,13 @@ export function useCurrentPlan(): { plan: PlanTier; isReady: boolean } { const activeSubscription = subscriptionsData.find((subscription) => isActiveSubscriptionStatus(subscription.status), ); + const subscriptionsLoaded = isReady || subscriptionsData.length > 0; const plan = resolveCurrentPlan({ subscriptionPlan: activeSubscription?.plan, sessionPlan: session?.session?.plan, - subscriptionsLoaded: isReady, + subscriptionsLoaded, }); - return { plan, isReady }; + return { plan, isReady: subscriptionsLoaded }; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/$automationId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/$automationId/page.tsx index ec30d445e2a..2e0ee030dcd 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/$automationId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/$automationId/page.tsx @@ -40,7 +40,7 @@ function AutomationDetailPage() { const collections = useCollections(); const [historyOpen, setHistoryOpen] = useState(history ?? false); - const { data: automationRows } = useLiveQuery( + const { data: automationRows, isReady: automationReady } = useLiveQuery( (q) => q .from({ a: collections.automations }) @@ -79,6 +79,7 @@ function AutomationDetailPage() { }); if (!automation) { + if (!automationReady) return null; return (
Automation not found. diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx index 5ccd95fb65e..edae923ae77 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx @@ -215,6 +215,8 @@ function AutomationsPage() { }; const rowGrid = scope === "team" ? ROW_GRID_TEAM : ROW_GRID_MINE; + const showAutomationLoading = !automationsReady && automations.length === 0; + const showMineEmptyState = visible.length === 0 && scope === "mine"; return (
@@ -331,7 +333,7 @@ function AutomationsPage() { )}
- {!automationsReady ? null : visible.length === 0 && scope === "mine" ? ( + {showAutomationLoading ? null : showMineEmptyState ? (
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/ResourceConsumption.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/ResourceConsumption.tsx index b3eeb5ace77..419f1d60d8e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/ResourceConsumption.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/ResourceConsumption.tsx @@ -181,29 +181,27 @@ function ResourceConsumptionContent({ getDetails: () => ({ surface }), }); - const { data: rawSidebarProjects = [], isReady: sidebarProjectsReady } = - useLiveQuery( - (q) => - q - .from({ sp: collections.v2SidebarProjects }) - .orderBy(({ sp }) => sp.tabOrder, "asc") - .select(({ sp }) => ({ projectId: sp.projectId })), - [collections], - ); + const { data: rawSidebarProjects = [] } = useLiveQuery( + (q) => + q + .from({ sp: collections.v2SidebarProjects }) + .orderBy(({ sp }) => sp.tabOrder, "asc") + .select(({ sp }) => ({ projectId: sp.projectId })), + [collections], + ); - const { data: rawSidebarWorkspaces = [], isReady: sidebarWorkspacesReady } = - useLiveQuery( - (q) => - q - .from({ ws: collections.v2WorkspaceLocalState }) - .orderBy(({ ws }) => ws.sidebarState.tabOrder, "asc") - .select(({ ws }) => ({ - workspaceId: ws.workspaceId, - isHidden: ws.sidebarState.isHidden, - paneLayout: ws.paneLayout, - })), - [collections], - ); + const { data: rawSidebarWorkspaces = [] } = useLiveQuery( + (q) => + q + .from({ ws: collections.v2WorkspaceLocalState }) + .orderBy(({ ws }) => ws.sidebarState.tabOrder, "asc") + .select(({ ws }) => ({ + workspaceId: ws.workspaceId, + isHidden: ws.sidebarState.isHidden, + paneLayout: ws.paneLayout, + })), + [collections], + ); const sidebarProjectOrder = useMemo( () => rawSidebarProjects.map((p) => p.projectId), @@ -223,7 +221,7 @@ function ResourceConsumptionContent({ [rawSidebarWorkspaces], ); - const { data: rawV2Projects = [], isReady: v2ProjectsReady } = useLiveQuery( + const { data: rawV2Projects = [] } = useLiveQuery( (q) => q.from({ project: collections.v2Projects }).select(({ project }) => ({ id: project.id, @@ -232,30 +230,21 @@ function ResourceConsumptionContent({ [collections], ); - const { data: rawV2Workspaces = [], isReady: v2WorkspacesReady } = - useLiveQuery( - (q) => - q - .from({ workspace: collections.v2Workspaces }) - .select(({ workspace }) => ({ - id: workspace.id, - projectId: workspace.projectId, - name: workspace.name, - })), - [collections], - ); + const { data: rawV2Workspaces = [] } = useLiveQuery( + (q) => + q + .from({ workspace: collections.v2Workspaces }) + .select(({ workspace }) => ({ + id: workspace.id, + projectId: workspace.projectId, + name: workspace.name, + })), + [collections], + ); - const v2MetadataReady = - !isV2 || - (Boolean(organizationId) && - sidebarProjectsReady && - sidebarWorkspacesReady && - v2ProjectsReady && - v2WorkspacesReady); const shouldQueryMetrics = shouldQueryResourceMonitor({ enabled: true, open: true, - metadataReady: v2MetadataReady, }); const { @@ -282,7 +271,6 @@ function ResourceConsumptionContent({ const normalizedSnapshot = useMemo(() => { const normalized = normalizeResourceMetricsSnapshot(snapshot); if (!normalized || !isV2) return normalized; - if (!v2MetadataReady) return null; const projectById = new Map( rawV2Projects.map((project) => [project.id, project]), @@ -312,14 +300,7 @@ function ResourceConsumptionContent({ }; }), }; - }, [ - snapshot, - isV2, - v2MetadataReady, - rawV2Projects, - rawV2Workspaces, - terminalTitleOverrides, - ]); + }, [snapshot, isV2, rawV2Projects, rawV2Workspaces, terminalTitleOverrides]); const getPaneName = (session: SessionMetrics): string => { if (isV2) { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/resourceConsumptionPolicy.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/resourceConsumptionPolicy.test.ts index b6a860773f3..7fb0c72db2d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/resourceConsumptionPolicy.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/resourceConsumptionPolicy.test.ts @@ -11,26 +11,23 @@ describe("resourceConsumptionPolicy", () => { shouldQueryResourceMonitor({ enabled: true, open: false, - metadataReady: true, }), ).toBe(false); expect(getResourceMonitorRefetchInterval(false)).toBe(false); } }); - test("enables polling only after the popover is open and metadata is ready", () => { + test("enables polling only after the popover is open", () => { expect( shouldQueryResourceMonitor({ enabled: true, open: true, - metadataReady: true, }), ).toBe(true); expect( shouldQueryResourceMonitor({ - enabled: true, + enabled: false, open: true, - metadataReady: false, }), ).toBe(false); expect(getResourceMonitorRefetchInterval(true)).toBe(2_000); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/resourceConsumptionPolicy.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/resourceConsumptionPolicy.ts index f8bd41b807a..f0939b89398 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/resourceConsumptionPolicy.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/resourceConsumptionPolicy.ts @@ -3,13 +3,11 @@ export const RESOURCE_MONITOR_REFETCH_INTERVAL_MS = 2_000; export function shouldQueryResourceMonitor({ enabled, open, - metadataReady, }: { enabled: boolean | undefined; open: boolean; - metadataReady: boolean; }): boolean { - return enabled === true && open && metadataReady; + return enabled === true && open; } export function getResourceMonitorRefetchInterval( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/hooks/useRemoteHostStatus/useRemoteHostStatus.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/hooks/useRemoteHostStatus/useRemoteHostStatus.ts index f3fa0348a4a..f045a0f860d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/hooks/useRemoteHostStatus/useRemoteHostStatus.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/hooks/useRemoteHostStatus/useRemoteHostStatus.ts @@ -67,7 +67,7 @@ export function useRemoteHostStatus( if (!workspace) return { status: "loading" }; if (isLocal) return { status: "skip" }; - if (!isReady) return { status: "loading" }; + if (!isReady && !hostRow) return { status: "loading" }; if (infoQuery.isSuccess) { const hostVersion = infoQuery.data.version; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx index b86ddc55411..8ce27300c85 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx @@ -75,7 +75,7 @@ function V2WorkspaceLayout() { const hostStatus = useRemoteHostStatus(canUseWorkspace ? workspace : null); - if (!workspaceId || !isReady || !workspaces) { + if (!workspaceId || !workspaces || (!workspace && !isReady)) { return
; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useDefaultV2TerminalPresets/useDefaultV2TerminalPresets.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useDefaultV2TerminalPresets/useDefaultV2TerminalPresets.ts index 6de22851c33..66d3f6fdb58 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useDefaultV2TerminalPresets/useDefaultV2TerminalPresets.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useDefaultV2TerminalPresets/useDefaultV2TerminalPresets.ts @@ -27,13 +27,15 @@ export function useDefaultV2TerminalPresets(hostUrl: string | null): void { ); const preferences = preferenceRows[0] ?? DEFAULT_V2_USER_PREFERENCES; + const presetsUsable = presetsReady || v2Presets.length > 0; + const preferencesUsable = preferencesReady || preferenceRows.length > 0; useEffect(() => { if ( !hostUrl || !agentsFetched || - !presetsReady || - !preferencesReady || + !presetsUsable || + !preferencesUsable || preferences.terminalPresetsInitialized ) { return; @@ -75,8 +77,8 @@ export function useDefaultV2TerminalPresets(hostUrl: string | null): void { collections.v2UserPreferences, hostUrl, preferences.terminalPresetsInitialized, - preferencesReady, - presetsReady, + preferencesUsable, + presetsUsable, v2Presets, ]); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/account/components/AccountSettings/AccountSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/account/components/AccountSettings/AccountSettings.tsx index 46f3b8013d8..d985b904f9d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/account/components/AccountSettings/AccountSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/account/components/AccountSettings/AccountSettings.tsx @@ -40,7 +40,7 @@ export function AccountSettings({ visibleItems }: AccountSettingsProps) { const [nameValue, setNameValue] = useState(""); const [avatarPreview, setAvatarPreview] = useState(null); - const { data: usersData, isLoading } = useLiveQuery( + const { data: usersData, isReady } = useLiveQuery( (q) => q.from({ users: collections.users }), [collections], ); @@ -110,7 +110,7 @@ export function AccountSettings({ visibleItems }: AccountSettingsProps) {
{showProfile && - (isLoading ? ( + (!isReady && !user ? ( ) : user ? ( <> diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/api-keys/components/ApiKeysSettings/ApiKeysSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/api-keys/components/ApiKeysSettings/ApiKeysSettings.tsx index 61e3e1ee778..a767c7635b0 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/api-keys/components/ApiKeysSettings/ApiKeysSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/api-keys/components/ApiKeysSettings/ApiKeysSettings.tsx @@ -43,10 +43,11 @@ export function ApiKeysSettings({ visibleItems }: ApiKeysSettingsProps) { const [showNewKeyDialog, setShowNewKeyDialog] = useState(false); const [newKeyName, setNewKeyName] = useState(""); const [newKeyValue, setNewKeyValue] = useState(""); - const { data: apiKeys, isLoading } = useLiveQuery( + const { data: apiKeysData, isReady } = useLiveQuery( (q) => q.from({ apiKeys: collections.apiKeys }), [collections], ); + const apiKeys = apiKeysData ?? []; const showApiKeysList = isItemVisible( SETTING_ITEM_ID.API_KEYS_LIST, @@ -143,7 +144,7 @@ export function ApiKeysSettings({ visibleItems }: ApiKeysSettingsProps) {
{showApiKeysList && - (isLoading ? ( + (!isReady && apiKeys.length === 0 ? (
{[1, 2, 3].map((i) => (
@@ -155,7 +156,7 @@ export function ApiKeysSettings({ visibleItems }: ApiKeysSettingsProps) {
))}
- ) : !apiKeys || apiKeys.length === 0 ? ( + ) : apiKeys.length === 0 ? (

No API keys yet.

diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsx index 993a6908d50..9657feabed0 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsx @@ -34,7 +34,7 @@ export function HostSettings({ hostId }: HostSettingsProps) { const currentUserId = session?.user?.id ?? null; const actions = useOptimisticCollectionActions(); - const { data: hostRows = [] } = useLiveQuery( + const { data: hostRows = [], isReady: hostReady } = useLiveQuery( (q) => q .from({ hosts: collections.v2Hosts }) @@ -121,6 +121,7 @@ export function HostSettings({ hostId }: HostSettingsProps) { }, [hostUserRows, currentUserId]); if (!host) { + if (!hostReady) return null; return (
Host not found in this organization. diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx index 643c8e8adbd..5586e55e9af 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/page.tsx @@ -20,7 +20,7 @@ function HostsIndexPage() { ? MOCK_ORG_ID : (session?.session?.activeOrganizationId ?? null); - const { data: hosts = [] } = useLiveQuery( + const { data: hosts = [], isReady } = useLiveQuery( (q) => q .from({ hosts: collections.v2Hosts }) @@ -52,6 +52,7 @@ function HostsIndexPage() { }, [firstHostId, navigate]); if (hosts.length === 0) { + if (!isReady) return null; return (
No hosts yet. diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/MembersSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/MembersSettings.tsx index 209b8de15e1..35c43b6208f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/MembersSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/MembersSettings.tsx @@ -41,7 +41,7 @@ export function MembersSettings({ visibleItems }: MembersSettingsProps) { visibleItems, ); - const { data: membersData, isLoading } = useLiveQuery( + const { data: membersData, isReady } = useLiveQuery( (q) => q .from({ members: collections.members }) @@ -121,7 +121,7 @@ export function MembersSettings({ visibleItems }: MembersSettingsProps) {

Team Members

{showMembersList && - (isLoading ? ( + (!isReady && members.length === 0 ? (
{[1, 2, 3].map((i) => (
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/PendingInvitations/PendingInvitations.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/PendingInvitations/PendingInvitations.tsx index 943c28f484f..7b616fc67c7 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/PendingInvitations/PendingInvitations.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/PendingInvitations/PendingInvitations.tsx @@ -40,7 +40,7 @@ export function PendingInvitations({ visibleItems, ); - const { data: invitationsData, isLoading } = useLiveQuery( + const { data: invitationsData, isReady } = useLiveQuery( (q) => q .from({ invitations: collections.invitations }) @@ -81,7 +81,7 @@ export function PendingInvitations({ visibleItems, ); - if (isLoading) { + if (!isReady && invitations.length === 0) { return (
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx index c06c16d83b3..03c7737e0de 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx @@ -81,7 +81,7 @@ export function OrganizationSettings({ const [logoPreview, setLogoPreview] = useState(null); const [nameValue, setNameValue] = useState(""); - const { data: organizations, isLoading } = useLiveQuery( + const { data: organizations, isReady } = useLiveQuery( (q) => q.from({ organizations: collections.organizations }), [collections], ); @@ -118,7 +118,7 @@ export function OrganizationSettings({ visibleItems, ); - const { data: membersData, isLoading: isMembersLoading } = useLiveQuery( + const { data: membersData, isReady: membersReady } = useLiveQuery( (q) => q .from({ members: collections.members }) @@ -220,7 +220,7 @@ export function OrganizationSettings({ ); } - if (isLoading || !organization) { + if (!organization && !isReady) { return (
@@ -239,6 +239,14 @@ export function OrganizationSettings({ ); } + if (!organization) { + return ( +
+

Organization not found.

+
+ ); + } + const showOrgSettings = showLogo || showName || showSlug || showId; const showMembersSection = showMembersList || @@ -392,7 +400,7 @@ export function OrganizationSettings({

- {isMembersLoading ? ( + {!membersReady && members.length === 0 ? (
{[0, 1, 2].map((i) => (
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx index 5db0608732d..584486bc71c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx @@ -21,10 +21,10 @@ function ProjectsIndexPage() { ? MOCK_ORG_ID : (session?.session?.activeOrganizationId ?? null); - const { data: groups = [] } = + const { data: groups = [], isLoading: groupsLoading } = electronTrpc.workspaces.getAllGrouped.useQuery(); - const { data: v2Projects = [] } = useLiveQuery( + const { data: v2Projects = [], isReady } = useLiveQuery( (q) => q .from({ projects: collections.v2Projects }) @@ -67,6 +67,7 @@ function ProjectsIndexPage() { const isEmpty = v2Projects.length === 0 && groups.length === 0; if (isEmpty) { + if (!isReady || groupsLoading) return null; return (
No projects yet. diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/teams/$teamId/components/TeamDetailSettings/TeamDetailSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/teams/$teamId/components/TeamDetailSettings/TeamDetailSettings.tsx index 9c83d2e6b0b..afb568cf53e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/teams/$teamId/components/TeamDetailSettings/TeamDetailSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/teams/$teamId/components/TeamDetailSettings/TeamDetailSettings.tsx @@ -53,7 +53,7 @@ export function TeamDetailSettings({ teamId }: TeamDetailSettingsProps) { const activeOrganizationId = session?.session?.activeOrganizationId; const currentUserId = session?.user?.id; - const { data: teamsData, isLoading: teamsLoading } = useLiveQuery( + const { data: teamsData, isReady: teamsReady } = useLiveQuery( (q) => q .from({ teams: collections.teams }) @@ -72,7 +72,7 @@ export function TeamDetailSettings({ teamId }: TeamDetailSettingsProps) { [collections], ); - const { data: membersRaw, isLoading: membersLoading } = useLiveQuery( + const { data: membersRaw, isReady: membersReady } = useLiveQuery( (q) => q .from({ tm: collections.teamMembers }) @@ -201,7 +201,7 @@ export function TeamDetailSettings({ teamId }: TeamDetailSettingsProps) { if (!activeOrganizationId) return null; - const isLoading = teamsLoading || membersLoading; + const isReady = teamsReady && membersReady; return (
@@ -267,7 +267,7 @@ export function TeamDetailSettings({ teamId }: TeamDetailSettingsProps) { )}
- {isLoading && members.length === 0 ? ( + {!isReady && members.length === 0 ? (
{[1, 2, 3].map((i) => (
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/teams/components/TeamsSettings/TeamsSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/teams/components/TeamsSettings/TeamsSettings.tsx index 45a51123ff2..3fee918c82c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/teams/components/TeamsSettings/TeamsSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/teams/components/TeamsSettings/TeamsSettings.tsx @@ -19,7 +19,7 @@ export function TeamsSettings() { const navigate = useNavigate(); const activeOrganizationId = session?.session?.activeOrganizationId; - const { data: teamsData, isLoading } = useLiveQuery( + const { data: teamsData, isReady } = useLiveQuery( (q) => q .from({ teams: collections.teams }) @@ -57,7 +57,7 @@ export function TeamsSettings() {
- {isLoading ? ( + {!isReady && teams.length === 0 ? (
{[1, 2, 3].map((i) => (
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/V2ProjectSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/V2ProjectSettings.tsx index 7439fe18095..07c09678441 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/V2ProjectSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/V2ProjectSettings.tsx @@ -48,7 +48,7 @@ export function V2ProjectSettings({ const targetHostUrl = useHostUrl(hostId); const targetHostId = hostId ?? machineId; - const { data: v2Project } = useLiveQuery( + const { data: v2Project, isReady } = useLiveQuery( (q) => q .from({ projects: collections.v2Projects }) @@ -111,7 +111,14 @@ export function V2ProjectSettings({ }); const project = v2Project?.[0]; - if (!project) return null; + if (!project) { + if (!isReady) return null; + return ( +
+ Project not found. +
+ ); + } return (
From e14a38ebfa6cf55a6e59e5213e158c28da96e295 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 20 May 2026 14:00:15 -0700 Subject: [PATCH 2/3] Document cache-first live query rendering --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index 01fb23dab14..0108ddafde4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,6 +80,7 @@ bun run clean:workspaces # Clean all workspace node_modules 6. **Plan & doc placement** - implementation plans go in `plans/` (cross-cutting) or `apps//plans/` (app-scoped); shipped plans move to `plans/done/`. Architecture/reference docs go in `/docs/`. Never drop `*_PLAN.md` at an app root or inside `src/`. 7. **Always fix lint warnings before pushing** - CI fails on Biome warnings, not just errors (the lint script treats warnings as errors). Run `bun run lint:fix` after edits and verify `bun run lint` exits 0 before `git push`. Never push code that produces lint output, even auto-fixable formatting. 8. **Linear ticket format** - all tickets (creation, drafting, grooming) follow `.agents/skills/ticket-format/SKILL.md`. Read that file before creating or grooming a ticket. +9. **TanStack DB / Electric live queries are cache-first** - `useLiveQuery` can return persisted rows in `data` while the collection is still not `isReady`. Always render existing rows first. Use `isReady` only to decide what to show when no row/data exists yet: no data + not ready = loading/skeleton/null; no data + ready = empty/not-found. Never hide, blank, or replace existing `data` just because `isReady` is false or `isLoading` is true. --- From 6154036103989824843a09270d7cd86bab06ec3d Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Wed, 20 May 2026 14:05:06 -0700 Subject: [PATCH 3/3] Address cache-first review feedback --- AGENTS.md | 2 +- .../_dashboard/automations/$automationId/page.tsx | 2 +- .../_authenticated/_dashboard/automations/page.tsx | 9 ++++++--- .../useDefaultV2TerminalPresets.ts | 10 ++++------ .../$hostId/components/HostSettings/HostSettings.tsx | 2 +- .../OrganizationSettings/OrganizationSettings.tsx | 4 +++- .../components/V2ProjectSettings/V2ProjectSettings.tsx | 2 +- 7 files changed, 17 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0108ddafde4..06b5aa9d21c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -80,7 +80,7 @@ bun run clean:workspaces # Clean all workspace node_modules 6. **Plan & doc placement** - implementation plans go in `plans/` (cross-cutting) or `apps//plans/` (app-scoped); shipped plans move to `plans/done/`. Architecture/reference docs go in `/docs/`. Never drop `*_PLAN.md` at an app root or inside `src/`. 7. **Always fix lint warnings before pushing** - CI fails on Biome warnings, not just errors (the lint script treats warnings as errors). Run `bun run lint:fix` after edits and verify `bun run lint` exits 0 before `git push`. Never push code that produces lint output, even auto-fixable formatting. 8. **Linear ticket format** - all tickets (creation, drafting, grooming) follow `.agents/skills/ticket-format/SKILL.md`. Read that file before creating or grooming a ticket. -9. **TanStack DB / Electric live queries are cache-first** - `useLiveQuery` can return persisted rows in `data` while the collection is still not `isReady`. Always render existing rows first. Use `isReady` only to decide what to show when no row/data exists yet: no data + not ready = loading/skeleton/null; no data + ready = empty/not-found. Never hide, blank, or replace existing `data` just because `isReady` is false or `isLoading` is true. +9. **TanStack DB / Electric live queries are cache-first** - `useLiveQuery` can return persisted rows in `data` while the collection is still not `isReady`. Always render existing rows first. Use `isReady` only to decide what to show when no row/data exists yet: no data + not ready = loading/skeleton/null; no data + ready = empty/not-found. Never hide, blank, or replace existing `data` just because `isReady` is false or `isLoading` is true. This cache-first rendering rule does not apply to write/seeding side effects: wait for strict readiness before deriving missing rows or writing defaults, unless the write is provably idempotent. --- diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/$automationId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/$automationId/page.tsx index 2e0ee030dcd..08058c9b1aa 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/$automationId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/$automationId/page.tsx @@ -81,7 +81,7 @@ function AutomationDetailPage() { if (!automation) { if (!automationReady) return null; return ( -
+
Automation not found.
); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx index edae923ae77..eb844997c86 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/page.tsx @@ -215,8 +215,11 @@ function AutomationsPage() { }; const rowGrid = scope === "team" ? ROW_GRID_TEAM : ROW_GRID_MINE; - const showAutomationLoading = !automationsReady && automations.length === 0; - const showMineEmptyState = visible.length === 0 && scope === "mine"; + const showAutomationLoading = !automationsReady && visible.length === 0; + const showMineEmptyState = + automationsReady && visible.length === 0 && scope === "mine"; + const showTeamEmptyState = + automationsReady && visible.length === 0 && scope === "team"; return (
@@ -337,7 +340,7 @@ function AutomationsPage() {
- ) : visible.length === 0 ? ( + ) : showTeamEmptyState ? ( 0; - const preferencesUsable = preferencesReady || preferenceRows.length > 0; useEffect(() => { if ( !hostUrl || !agentsFetched || - !presetsUsable || - !preferencesUsable || + !presetsReady || + !preferencesReady || preferences.terminalPresetsInitialized ) { return; @@ -77,8 +75,8 @@ export function useDefaultV2TerminalPresets(hostUrl: string | null): void { collections.v2UserPreferences, hostUrl, preferences.terminalPresetsInitialized, - preferencesUsable, - presetsUsable, + preferencesReady, + presetsReady, v2Presets, ]); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsx index 9657feabed0..178b02be496 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/hosts/$hostId/components/HostSettings/HostSettings.tsx @@ -123,7 +123,7 @@ export function HostSettings({ hostId }: HostSettingsProps) { if (!host) { if (!hostReady) return null; return ( -
+
Host not found in this organization.
); diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx index 03c7737e0de..7934832cfeb 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx @@ -242,7 +242,9 @@ export function OrganizationSettings({ if (!organization) { return (
-

Organization not found.

+

+ Organization not found. +

); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/V2ProjectSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/V2ProjectSettings.tsx index 07c09678441..d236f20265d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/V2ProjectSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/v2-project/$projectId/components/V2ProjectSettings/V2ProjectSettings.tsx @@ -114,7 +114,7 @@ export function V2ProjectSettings({ if (!project) { if (!isReady) return null; return ( -
+
Project not found.
);