Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<app>/plans/` (app-scoped); shipped plans move to `plans/done/`. Architecture/reference docs go in `<app>/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. 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.


---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/src/renderer/hooks/useCurrentPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -79,8 +79,9 @@ function AutomationDetailPage() {
});

if (!automation) {
if (!automationReady) return null;
return (
<div className="flex h-full w-full items-center justify-center text-sm text-muted-foreground">
<div className="flex h-full w-full items-center justify-center text-sm text-muted-foreground select-text cursor-text">
Automation not found.
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ function AutomationsPage() {
};

const rowGrid = scope === "team" ? ROW_GRID_TEAM : ROW_GRID_MINE;
const showAutomationLoading = !automationsReady && visible.length === 0;
const showMineEmptyState =
automationsReady && visible.length === 0 && scope === "mine";
const showTeamEmptyState =
automationsReady && visible.length === 0 && scope === "team";

return (
<div className="flex h-full w-full flex-1 flex-col overflow-hidden">
Expand Down Expand Up @@ -331,11 +336,11 @@ function AutomationsPage() {
)}

<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
{!automationsReady ? null : visible.length === 0 && scope === "mine" ? (
{showAutomationLoading ? null : showMineEmptyState ? (
<div className="flex-1 overflow-y-auto px-8 py-8">
<AutomationsEmptyState onSelectTemplate={handleSelectTemplate} />
</div>
) : visible.length === 0 ? (
) : showTeamEmptyState ? (
<Empty className="flex-1">
<EmptyHeader>
<EmptyMedia
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -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]),
Expand Down Expand Up @@ -312,14 +300,7 @@ function ResourceConsumptionContent({
};
}),
};
}, [
snapshot,
isV2,
v2MetadataReady,
rawV2Projects,
rawV2Workspaces,
terminalTitleOverrides,
]);
}, [snapshot, isV2, rawV2Projects, rawV2Workspaces, terminalTitleOverrides]);

const getPaneName = (session: SessionMetrics): string => {
if (isV2) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function V2WorkspaceLayout() {

const hostStatus = useRemoteHostStatus(canUseWorkspace ? workspace : null);

if (!workspaceId || !isReady || !workspaces) {
if (!workspaceId || !workspaces || (!workspace && !isReady)) {
return <div className="flex h-full w-full" />;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function AccountSettings({ visibleItems }: AccountSettingsProps) {
const [nameValue, setNameValue] = useState("");
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);

const { data: usersData, isLoading } = useLiveQuery(
const { data: usersData, isReady } = useLiveQuery(
(q) => q.from({ users: collections.users }),
[collections],
);
Expand Down Expand Up @@ -110,7 +110,7 @@ export function AccountSettings({ visibleItems }: AccountSettingsProps) {

<div className="space-y-3">
{showProfile &&
(isLoading ? (
(!isReady && !user ? (
<ProfileSkeleton />
) : user ? (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -143,7 +144,7 @@ export function ApiKeysSettings({ visibleItems }: ApiKeysSettingsProps) {
</div>

{showApiKeysList &&
(isLoading ? (
(!isReady && apiKeys.length === 0 ? (
<div className="divide-y divide-border">
{[1, 2, 3].map((i) => (
<div key={i} className="flex items-center gap-4 py-3">
Expand All @@ -155,7 +156,7 @@ export function ApiKeysSettings({ visibleItems }: ApiKeysSettingsProps) {
</div>
))}
</div>
) : !apiKeys || apiKeys.length === 0 ? (
) : apiKeys.length === 0 ? (
<div className="text-center py-12 text-sm text-muted-foreground">
<HiOutlineKey className="h-8 w-8 mx-auto mb-3 opacity-50" />
<p>No API keys yet.</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -121,8 +121,9 @@ export function HostSettings({ hostId }: HostSettingsProps) {
}, [hostUserRows, currentUserId]);

if (!host) {
if (!hostReady) return null;
return (
<div className="p-6 text-sm text-muted-foreground">
<div className="p-6 text-sm text-muted-foreground select-text cursor-text">
Host not found in this organization.
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -52,6 +52,7 @@ function HostsIndexPage() {
}, [firstHostId, navigate]);

if (hosts.length === 0) {
if (!isReady) return null;
return (
<div className="flex items-center justify-center h-full p-6 text-sm text-muted-foreground">
No hosts yet.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -121,7 +121,7 @@ export function MembersSettings({ visibleItems }: MembersSettingsProps) {
<h3 className="text-lg font-semibold">Team Members</h3>

{showMembersList &&
(isLoading ? (
(!isReady && members.length === 0 ? (
<div className="space-y-2 border rounded-lg">
{[1, 2, 3].map((i) => (
<div key={i} className="flex items-center gap-4 p-4">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function PendingInvitations({
visibleItems,
);

const { data: invitationsData, isLoading } = useLiveQuery(
const { data: invitationsData, isReady } = useLiveQuery(
(q) =>
q
.from({ invitations: collections.invitations })
Expand Down Expand Up @@ -81,7 +81,7 @@ export function PendingInvitations({
visibleItems,
);

if (isLoading) {
if (!isReady && invitations.length === 0) {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
Expand Down
Loading
Loading