Skip to content
Open
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
2 changes: 1 addition & 1 deletion apps/app/.ladle/settings-story-fixtures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ function createSettingsStoryQueryClient() {
hostProviderCliStatusQueryKey(HOST_IDS.remote),
remoteProviderStatus,
);
queryClient.setQueryData(pluginListQueryKey(true), { plugins: [] });
queryClient.setQueryData(pluginListQueryKey(true), []);
return queryClient;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createQueryClientTestHarness } from "@/test/queryClientTestHarness";
import type { PluginListResult } from "@/hooks/queries/plugin-settings-queries";
import type { InstalledPlugin } from "@bb/server-contract";
import {
pluginCatalogSearchQueryKey,
pluginListQueryKey,
Expand Down Expand Up @@ -332,9 +332,7 @@ describe("AddPluginDialog", () => {
stubFetch();
const onInstalled = vi.fn();
const { wrapper, queryClient } = createQueryClientTestHarness();
queryClient.setQueryData<PluginListResult>(pluginListQueryKey(true), {
plugins: [],
});
queryClient.setQueryData<InstalledPlugin[]>(pluginListQueryKey(true), []);
render(
<AddPluginDialog
open
Expand All @@ -353,8 +351,8 @@ describe("AddPluginDialog", () => {
onInstalled(plugin);
expect(
queryClient
.getQueryData<PluginListResult>(pluginListQueryKey(true))
?.plugins.some((candidate) => candidate.id === plugin.id),
.getQueryData<InstalledPlugin[]>(pluginListQueryKey(true))
?.some((candidate) => candidate.id === plugin.id),
).toBe(true);
}}
/>,
Expand Down
23 changes: 8 additions & 15 deletions apps/app/src/hooks/cache-owners/plugin-cache-owner.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import type { QueryClient } from "@tanstack/react-query";
import {
toPluginListItem,
type PluginListResult,
type PluginSettingsView,
} from "../queries/plugin-settings-queries";
import { type PluginSettingsView } from "../queries/plugin-settings-queries";
import type { InstalledPlugin } from "@bb/server-contract";
import {
allPluginCatalogSearchQueryKeyPrefix,
Expand All @@ -28,22 +24,19 @@ export function applyInstalledPlugin(args: {
queryClient: QueryClient;
plugin: InstalledPlugin;
}): void {
const installed = toPluginListItem(args.plugin);
args.queryClient.setQueryData<PluginListResult>(
args.queryClient.setQueryData<InstalledPlugin[]>(
pluginListQueryKey(true),
(current) => {
const plugins = current?.plugins ?? [];
const plugins = current ?? [];
const existingIndex = plugins.findIndex(
(candidate) => candidate.id === installed.id,
(candidate) => candidate.id === args.plugin.id,
);
if (existingIndex === -1) {
return { plugins: [...plugins, installed] };
return [...plugins, args.plugin];
}
return {
plugins: plugins.map((candidate, index) =>
index === existingIndex ? installed : candidate,
),
};
return plugins.map((candidate, index) =>
index === existingIndex ? args.plugin : candidate,
);
},
);
}
Expand Down
45 changes: 43 additions & 2 deletions apps/app/src/hooks/queries/plugin-settings-queries.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { describe, expect, it } from "vitest";
import { fetchPluginList, removePlugin } from "./plugin-settings-queries";
import { afterEach, describe, expect, it, vi } from "vitest";
import { QueryClient } from "@tanstack/react-query";
import { fetchFrontendCandidates } from "@/lib/plugin-frontend";
import { pluginListQueryKey } from "./query-keys";
import {
fetchInstalledPlugins,
fetchPluginList,
removePlugin,
} from "./plugin-settings-queries";

function fetchReturning(body: unknown, status = 200): typeof fetch {
return async () =>
Expand Down Expand Up @@ -52,7 +59,41 @@ const ROW = {
logoDarkUrl: null,
};

afterEach(() => {
vi.unstubAllGlobals();
});

describe("fetchPluginList envelope", () => {
it("lets the frontend loader reuse the app plugin list", async () => {
const plugin = {
...ROW,
app: {
hasApp: true,
bundle: {
jsUrl: "/api/v1/plugins/linear/assets/app.js?h=abc",
cssUrl: null,
jsBytes: 1_000,
hash: "abc",
sdkMajor: 0,
sdkVersion: "0.4.27",
compatible: true,
},
},
};
const queryClient = new QueryClient();
queryClient.setQueryData(
pluginListQueryKey(true),
await fetchInstalledPlugins(fetchReturning({ plugins: [plugin] })),
);
const networkFetch = vi.fn(fetchReturning({ plugins: [plugin] }));
vi.stubGlobal("fetch", networkFetch);

await expect(fetchFrontendCandidates(queryClient)).resolves.toEqual([
expect.objectContaining({ pluginId: "linear" }),
]);
expect(networkFetch).not.toHaveBeenCalled();
});

it("binds browser fetch before the SDK invokes it", async () => {
const result = await fetchPluginList(
receiverSensitiveFetch({ plugins: [ROW] }),
Expand Down
29 changes: 23 additions & 6 deletions apps/app/src/hooks/queries/plugin-settings-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
PluginSettingsResponse,
} from "@bb/server-contract";
import { pluginSettingsUpdateRequestSchema } from "@bb/server-contract";
import { useQuery } from "@tanstack/react-query";
import { queryOptions, useQuery } from "@tanstack/react-query";
import { createPluginsClient } from "./plugin-client";
import { pluginListQueryKey, pluginSettingsViewQueryKey } from "./query-keys";

Expand Down Expand Up @@ -131,9 +131,17 @@ export function toPluginListItem(plugin: InstalledPlugin): PluginListItem {

export async function fetchPluginList(
fetchImpl: FetchLike,
signal?: AbortSignal,
): Promise<PluginListResult> {
const result = await createPluginsClient(fetchImpl).list();
return { plugins: result.plugins.map(toPluginListItem) };
const plugins = await fetchInstalledPlugins(fetchImpl, signal);
return { plugins: plugins.map(toPluginListItem) };
}

export async function fetchInstalledPlugins(
fetchImpl: FetchLike,
signal?: AbortSignal,
): Promise<InstalledPlugin[]> {
return (await createPluginsClient(fetchImpl).list({ signal })).plugins;
}

export type PluginSettingFieldDescriptor = PluginSettingDescriptor;
Expand Down Expand Up @@ -194,15 +202,24 @@ export async function removePlugin(
await createPluginsClient(fetchImpl).remove({ pluginId });
}

export function usePluginList(args: { enabled: boolean }) {
return useQuery({
export function pluginListQueryOptions(args: { enabled: boolean }) {
return queryOptions({
queryKey: pluginListQueryKey(args.enabled),
queryFn: () => fetchPluginList(fetch),
queryFn: ({ signal }) => fetchInstalledPlugins(fetch, signal),
enabled: args.enabled,
staleTime: 30_000,
});
}

export function usePluginList(args: { enabled: boolean }) {
return useQuery({
...pluginListQueryOptions(args),
select: (plugins): PluginListResult => ({
plugins: plugins.map(toPluginListItem),
}),
});
}

export function usePluginSettingsView(
pluginId: string,
options: { enabled: boolean },
Expand Down
22 changes: 16 additions & 6 deletions apps/app/src/hooks/queries/system-queries.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
import {
queryOptions,
useQueries,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import type { QueryKey } from "@tanstack/react-query";
import type {
PermissionMode,
Expand All @@ -10,7 +15,6 @@ import { permissionModeValues } from "@bb/domain";
import { toRecord } from "@bb/core-ui";
import type {
SystemCliSkillsStatusResponse,
SystemConfigResponse,
SystemExecutionOptionsResponse,
SystemProvidersQuery,
SystemProviderStatesResponse,
Expand Down Expand Up @@ -319,15 +323,21 @@ export function useSystemExecutionOptions(
});
}

export function systemConfigQueryOptions() {
return queryOptions({
queryKey: systemConfigQueryKey(),
queryFn: ({ signal }) => sdk.system.config({ signal }),
staleTime: 60_000,
});
}

export function useSystemConfig(options?: QueryOptions) {
const enabled = options?.enabled ?? true;
useSystemRealtimeSubscription({ enabled });

return useQuery<SystemConfigResponse>({
queryKey: systemConfigQueryKey(),
queryFn: ({ signal }) => sdk.system.config({ signal }),
return useQuery({
...systemConfigQueryOptions(),
enabled,
staleTime: 60_000,
});
}

Expand Down
50 changes: 50 additions & 0 deletions apps/app/src/hooks/queries/thread-queries.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
sidebarNavigationQueryKey,
threadDetailBootstrapQueryKey,
threadHostFilePreviewQueryKey,
threadPendingInteractionsQueryKey,
threadQueuedMessagesQueryKey,
threadQueryKey,
threadTimelineQueryKey,
Expand All @@ -31,6 +32,7 @@ import {
useThreadDetailBootstrap,
useThreadHostFilePreview,
useThreadMentionCandidates,
useThreadPendingInteractions,
useThreadQueuedMessages,
useThreadStorageLocation,
useThreadTimeline,
Expand All @@ -50,6 +52,7 @@ vi.mock("@/lib/sdk", () => ({
get: vi.fn(),
list: vi.fn(),
queuedMessages: { list: vi.fn() },
interactions: { list: vi.fn() },
storageLocation: vi.fn(),
timeline: vi.fn(),
},
Expand Down Expand Up @@ -148,6 +151,7 @@ beforeEach(() => {
vi.mocked(sdk.threads.get).mockResolvedValue(THREAD_WITH_INCLUDES);
vi.mocked(sdk.threads.list).mockResolvedValue([]);
vi.mocked(sdk.threads.queuedMessages.list).mockResolvedValue([]);
vi.mocked(sdk.threads.interactions.list).mockResolvedValue([]);
vi.mocked(sdk.threads.storageLocation).mockResolvedValue({
hostId: "host-1",
storageRootPath: "/tmp/thread-storage/thread-1",
Expand Down Expand Up @@ -443,6 +447,52 @@ describe("useThreadQueuedMessages", () => {
});
});

describe("useThreadPendingInteractions", () => {
it("reuses the first owner's fresh baseline when a second owner mounts", async () => {
const { queryClient, wrapper } = createQueryClientTestHarness();
const first = renderHook(() => useThreadPendingInteractions("thread-1"), {
wrapper,
});
await waitFor(() => {
expect(first.result.current.isSuccess).toBe(true);
});
queryClient.setQueryData(
threadPendingInteractionsQueryKey("thread-1"),
[],
{ updatedAt: Date.now() - 1_000 },
);

renderHook(() => useThreadPendingInteractions("thread-1"), { wrapper });
await act(async () => {
await Promise.resolve();
});

expect(sdk.threads.interactions.list).toHaveBeenCalledTimes(1);
});

it("refetches the interaction baseline when a stale owner remounts", async () => {
const { queryClient, wrapper } = createQueryClientTestHarness();
const first = renderHook(() => useThreadPendingInteractions("thread-1"), {
wrapper,
});
await waitFor(() => {
expect(first.result.current.isSuccess).toBe(true);
});
first.unmount();
queryClient.setQueryData(
threadPendingInteractionsQueryKey("thread-1"),
[],
{ updatedAt: Date.now() - 2_500 },
);

renderHook(() => useThreadPendingInteractions("thread-1"), { wrapper });

await waitFor(() => {
expect(sdk.threads.interactions.list).toHaveBeenCalledTimes(2);
});
});
});

describe("useThreadHostFilePreview", () => {
it("refetches stale host file previews on focus and reconnect", async () => {
const { queryClient, wrapper } = createQueryClientTestHarness();
Expand Down
4 changes: 3 additions & 1 deletion apps/app/src/hooks/queries/thread-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,9 @@ export function useThreadPendingInteractions(
enabled,
refetchOnMount: options?.refetchOnMount ?? true,
...REALTIME_OWNED_NO_FOCUS_QUERY_POLICY,
staleTime: options?.staleTime,
...(options?.staleTime === undefined
? {}
: { staleTime: options.staleTime }),
});
}

Expand Down
7 changes: 7 additions & 0 deletions apps/app/src/lib/app-query-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createAppQueryClient } from "./query-client";
import { wsManager } from "./ws";

export const appQueryClient = createAppQueryClient({
shouldRefetchOnWindowFocus: () =>
wsManager.getConnectionState() !== "connected",
});
Loading
Loading