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
4 changes: 4 additions & 0 deletions apps/app/src/hooks/cache-owners/cache-owner-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const CACHE_OWNER_QUERY_KEY_IMPORTS: CacheOwnerQueryKeyImportRegistry = {
"threadQueuedMessagesQueryKey",
"threadStorageFilePreviewQueryKeyPrefix",
"threadStorageFilesForThreadQueryKeyPrefix",
"threadStorageLocationQueryKey",
"threadStoragePathsForThreadQueryKeyPrefix",
"threadTimelineQueryKeyPrefix",
"threadTimelineTurnSummaryDetailsQueryKeyPrefix",
Expand Down Expand Up @@ -151,6 +152,7 @@ const CACHE_OWNER_QUERY_KEY_IMPORTS: CacheOwnerQueryKeyImportRegistry = {
"allSystemProvidersQueryKeyPrefix",
"allThreadStorageFilePreviewQueryKeyPrefix",
"allThreadStorageFilesQueryKeyPrefix",
"allThreadStorageLocationsQueryKeyPrefix",
"allThreadStoragePathsQueryKeyPrefix",
"allThreadQueryKeyPrefix",
"allTerminalsQueryKeyPrefix",
Expand All @@ -167,6 +169,7 @@ const CACHE_OWNER_QUERY_KEY_IMPORTS: CacheOwnerQueryKeyImportRegistry = {
"threadSearchQueryKeyPrefix",
"threadStorageFilePreviewQueryKeyPrefix",
"threadStorageFilesForThreadQueryKeyPrefix",
"threadStorageLocationQueryKey",
"threadStoragePathsForThreadQueryKeyPrefix",
"threadTimelineQueryKeyPrefix",
"terminalsQueryKey",
Expand All @@ -193,6 +196,7 @@ const CACHE_OWNER_QUERY_KEY_IMPORTS: CacheOwnerQueryKeyImportRegistry = {
"allThreadQueuedMessagesQueryKeyPrefix",
"allThreadStorageFilePreviewQueryKeyPrefix",
"allThreadStorageFilesQueryKeyPrefix",
"allThreadStorageLocationsQueryKeyPrefix",
"allThreadStoragePathsQueryKeyPrefix",
"allThreadTimelineQueryKeyPrefix",
"allThreadTimelineTurnSummaryDetailsQueryKeyPrefix",
Expand Down
4 changes: 4 additions & 0 deletions apps/app/src/hooks/cache-owners/mutation-cache-effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
threadsQueryKey,
threadStorageFilePreviewQueryKeyPrefix,
threadStorageFilesForThreadQueryKeyPrefix,
threadStorageLocationQueryKey,
threadStoragePathsForThreadQueryKeyPrefix,
threadTimelineQueryKeyPrefix,
threadTimelineTurnSummaryDetailsQueryKeyPrefix,
Expand Down Expand Up @@ -330,6 +331,9 @@ export function removeThreadScopedQueries({
queryClient.removeQueries({
queryKey: threadStorageFilesForThreadQueryKeyPrefix(threadId),
});
queryClient.removeQueries({
queryKey: threadStorageLocationQueryKey(threadId),
});
queryClient.removeQueries({
queryKey: threadStoragePathsForThreadQueryKeyPrefix(threadId),
});
Expand Down
4 changes: 4 additions & 0 deletions apps/app/src/hooks/cache-owners/realtime-cache-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import {
allProjectCommandsQueryKeyPrefix,
allThreadStorageFilePreviewQueryKeyPrefix,
allThreadStorageFilesQueryKeyPrefix,
allThreadStorageLocationsQueryKeyPrefix,
allThreadStoragePathsQueryKeyPrefix,
allSystemExecutionOptionsQueryKeyPrefix,
allThreadQueryKeyPrefix,
Expand All @@ -93,6 +94,7 @@ import {
threadsQueryKey,
threadStorageFilePreviewQueryKeyPrefix,
threadStorageFilesForThreadQueryKeyPrefix,
threadStorageLocationQueryKey,
threadStoragePathsForThreadQueryKeyPrefix,
threadTimelineQueryKeyPrefix,
} from "../queries/query-keys";
Expand Down Expand Up @@ -1001,12 +1003,14 @@ function dirtyThreadStorageQueriesForThread({
if (!threadId) {
return [
allThreadStorageFilesQueryKeyPrefix(),
allThreadStorageLocationsQueryKeyPrefix(),
allThreadStoragePathsQueryKeyPrefix(),
allThreadStorageFilePreviewQueryKeyPrefix(),
];
}
return [
threadStorageFilesForThreadQueryKeyPrefix(threadId),
threadStorageLocationQueryKey(threadId),
threadStoragePathsForThreadQueryKeyPrefix(threadId),
threadStorageFilePreviewQueryKeyPrefix(threadId),
];
Expand Down
2 changes: 2 additions & 0 deletions apps/app/src/hooks/cache-owners/system-cache-effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
allThreadQueryKeyPrefix,
allThreadStorageFilePreviewQueryKeyPrefix,
allThreadStorageFilesQueryKeyPrefix,
allThreadStorageLocationsQueryKeyPrefix,
allThreadStoragePathsQueryKeyPrefix,
allThreadTimelineQueryKeyPrefix,
allThreadTimelineTurnSummaryDetailsQueryKeyPrefix,
Expand Down Expand Up @@ -186,6 +187,7 @@ function getServerReconnectInvalidationQueryKeys(): QueryKey[] {
allThreadPendingInteractionsQueryKeyPrefix(),
allThreadDefaultExecutionOptionsQueryKeyPrefix(),
allThreadStorageFilesQueryKeyPrefix(),
allThreadStorageLocationsQueryKeyPrefix(),
allThreadStoragePathsQueryKeyPrefix(),
allThreadStorageFilePreviewQueryKeyPrefix(),
allThreadHostFilePreviewQueryKeyPrefix(),
Expand Down
18 changes: 18 additions & 0 deletions apps/app/src/hooks/queries/query-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const THREAD_PENDING_INTERACTIONS_QUERY_KEY =
export const TERMINALS_QUERY_KEY = "terminals";
export const PROJECT_COMMANDS_QUERY_KEY = "projectCommands";
export const THREAD_STORAGE_FILES_QUERY_KEY = "threadStorageFiles";
export const THREAD_STORAGE_LOCATION_QUERY_KEY = "threadStorageLocation";
export const THREAD_STORAGE_PATHS_QUERY_KEY = "threadStoragePaths";
export const THREAD_STORAGE_FILE_PREVIEW_QUERY_KEY = "threadStorageFilePreview";
export const THREAD_HOST_FILE_PREVIEW_QUERY_KEY = "threadHostFilePreview";
Expand Down Expand Up @@ -255,6 +256,13 @@ export type ThreadStorageFilesQueryKey = readonly [
string,
ThreadStorageFileListOptions,
];
export type ThreadStorageLocationQueryKey = readonly [
typeof THREAD_STORAGE_LOCATION_QUERY_KEY,
string,
];
export type AllThreadStorageLocationsQueryKeyPrefix = readonly [
typeof THREAD_STORAGE_LOCATION_QUERY_KEY,
];
export type ThreadStoragePathsQueryKey = readonly [
typeof THREAD_STORAGE_PATHS_QUERY_KEY,
string,
Expand Down Expand Up @@ -768,6 +776,16 @@ export function threadStorageFilesQueryKey(
return [THREAD_STORAGE_FILES_QUERY_KEY, threadId, options];
}

export function threadStorageLocationQueryKey(
threadId: string,
): ThreadStorageLocationQueryKey {
return [THREAD_STORAGE_LOCATION_QUERY_KEY, threadId];
}

export function allThreadStorageLocationsQueryKeyPrefix(): AllThreadStorageLocationsQueryKeyPrefix {
Comment thread
ymichael marked this conversation as resolved.
return [THREAD_STORAGE_LOCATION_QUERY_KEY];
}

export function threadStoragePathsQueryKey(
threadId: string,
options: PathListOptions = DEFAULT_FILE_ONLY_PATH_LIST_OPTIONS,
Expand Down
27 changes: 27 additions & 0 deletions apps/app/src/hooks/queries/thread-queries.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
useThreadHostFilePreview,
useThreadMentionCandidates,
useThreadQueuedMessages,
useThreadStorageLocation,
useThreadTimeline,
} from "./thread-queries";

Expand All @@ -49,6 +50,7 @@ vi.mock("@/lib/sdk", () => ({
get: vi.fn(),
list: vi.fn(),
queuedMessages: { list: vi.fn() },
storageLocation: vi.fn(),
timeline: vi.fn(),
},
},
Expand Down Expand Up @@ -146,6 +148,10 @@ 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.storageLocation).mockResolvedValue({
hostId: "host-1",
storageRootPath: "/tmp/thread-storage/thread-1",
});
vi.mocked(sdk.threads.timeline).mockResolvedValue({
rows: [],
activePromptMode: null,
Expand Down Expand Up @@ -680,6 +686,27 @@ describe("useThreadMentionCandidates", () => {
});
});

describe("useThreadStorageLocation", () => {
it("requests only the storage location for the thread", async () => {
const { wrapper } = createQueryClientTestHarness();

const { result } = renderHook(() => useThreadStorageLocation("thread-1"), {
wrapper,
});

await waitFor(() => {
expect(result.current.data).toEqual({
hostId: "host-1",
storageRootPath: "/tmp/thread-storage/thread-1",
});
});
expect(sdk.threads.storageLocation).toHaveBeenCalledWith({
threadId: "thread-1",
signal: expect.any(AbortSignal),
});
});
});

describe("useThreadTimeline segment limit", () => {
it("asks for the compact first window on compact viewports and keeps it for deltas", async () => {
mockMatchMedia([COMPACT_VIEWPORT_QUERY]);
Expand Down
18 changes: 18 additions & 0 deletions apps/app/src/hooks/queries/thread-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
ThreadWithIncludesResponse,
ThreadConversationOutlineResponse,
ThreadStorageFileListResponse,
ThreadStorageLocationResponse,
ThreadStoragePathListResponse,
ThreadTimelineResponse,
TimelineTurnSummaryDetailsResponse,
Expand Down Expand Up @@ -71,6 +72,7 @@ import {
threadQueryKey,
threadSearchQueryKey,
threadStorageFilesQueryKey,
threadStorageLocationQueryKey,
threadStoragePathsQueryKey,
threadStorageFilePreviewQueryKey,
threadHostFilePreviewQueryKey,
Expand Down Expand Up @@ -811,6 +813,22 @@ export function useThreadStorageFiles(
});
}

export function useThreadStorageLocation(id: string, options?: QueryOptions) {
const enabled = (options?.enabled ?? true) && Boolean(id);
useThreadDetailRealtimeSubscription(id, { enabled });

return useQuery<ThreadStorageLocationResponse>({
queryKey: threadStorageLocationQueryKey(id),
queryFn: ({ signal }) =>
sdk.threads.storageLocation({
threadId: requireThreadId(id, "useThreadStorageLocation"),
signal,
}),
enabled,
...REALTIME_OWNED_MOUNT_BASELINE_QUERY_POLICY,
});
}

export function useThreadStoragePaths(
id: string,
listOptions: PathListOptions,
Expand Down
5 changes: 3 additions & 2 deletions apps/app/src/hooks/system-cache-effects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
threadQueryKey,
threadQueuedMessagesQueryKey,
threadSearchQueryKey,
threadStorageLocationQueryKey,
threadTimelineQueryKey,
} from "./queries/query-keys";
import {
Expand Down Expand Up @@ -324,10 +325,10 @@ describe("system cache effects", () => {
queryClient.clear();
});

it("refetches errored queries after reconnect", async () => {
it("recovers a failed active thread-storage location query after reconnect", async () => {
const queryClient = createCacheEffectQueryClient();
queryClient.mount();
const erroredKey = hostsQueryKey();
const erroredKey = threadStorageLocationQueryKey("thread-1");
const queryFn = vi
.fn<() => Promise<string>>()
.mockRejectedValueOnce(new Error("server restarting"))
Expand Down
113 changes: 113 additions & 0 deletions apps/app/src/hooks/useResolvedLiveFileTarget.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// @vitest-environment jsdom

import { cleanup, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useResolvedLiveFileTarget } from "./useResolvedLiveFileTarget";

const mocks = vi.hoisted(() => ({
isLocalDaemonHost: vi.fn(),
useEnvironment: vi.fn(),
useThreadStorageLocation: vi.fn(),
}));

vi.mock("@/hooks/queries/environment-queries", () => ({
useEnvironment: mocks.useEnvironment,
}));

vi.mock("@/hooks/queries/thread-queries", () => ({
useThreadStorageLocation: mocks.useThreadStorageLocation,
}));

vi.mock("@/hooks/useHostDaemon", () => ({
useHostDaemon: () => ({
isLocalDaemonHost: mocks.isLocalDaemonHost,
}),
}));

const target = {
kind: "thread-storage",
path: "reports/summary.md",
threadId: "thr_1",
} as const;

beforeEach(() => {
mocks.isLocalDaemonHost.mockReturnValue(false);
mocks.useEnvironment.mockReturnValue({
data: undefined,
isLoading: false,
});
mocks.useThreadStorageLocation.mockReturnValue({
data: {
hostId: "host_remote",
storageRootPath: "/var/lib/bb/thread-storage/thr_1",
},
isError: false,
isLoading: false,
});
});

afterEach(() => {
cleanup();
vi.clearAllMocks();
});

describe("useResolvedLiveFileTarget", () => {
it.each([
{
isLocal: true,
openContext: { kind: "local" },
},
{
isLocal: false,
openContext: {
kind: "remote-ssh",
hostId: "host_remote",
serverOrigin: window.location.origin,
},
},
] as const)(
"resolves thread storage from the direct location lookup when local is $isLocal",
({ isLocal, openContext }) => {
mocks.isLocalDaemonHost.mockReturnValue(isLocal);

const { result } = renderHook(() =>
useResolvedLiveFileTarget(target, { enabled: true }),
);

expect(result.current).toEqual({
status: "available",
absolutePath: "/var/lib/bb/thread-storage/thr_1/reports/summary.md",
hostId: "host_remote",
openContext,
});
expect(mocks.useThreadStorageLocation).toHaveBeenCalledWith("thr_1", {
enabled: true,
});
expect(mocks.useEnvironment).toHaveBeenCalledWith("", {
enabled: false,
});
},
);

it.each([
{
query: { data: undefined, isError: false, isLoading: true },
status: "loading",
},
{
query: { data: undefined, isError: true, isLoading: false },
status: "unavailable",
},
] as const)(
"preserves the $status storage lookup state",
({ query, status }) => {
mocks.useThreadStorageLocation.mockReturnValue(query);

const { result } = renderHook(() =>
useResolvedLiveFileTarget(target, { enabled: true }),
);

expect(result.current).toEqual({ status });
},
);
});
Loading
Loading