Skip to content
Merged
45 changes: 41 additions & 4 deletions apps/web/src/domains/chat/api/global-search.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,49 @@
import { searchGlobalGet } from "@/generated/daemon/sdk.gen";
import type { SearchGlobalGetResponse } from "@/generated/daemon/types.gen";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/** Search results grouped by category, as returned by the daemon. */
export type GlobalSearchResponse = SearchGlobalGetResponse["results"];
/**
* Search results grouped by category, as returned by the daemon.
*
* The generated `SearchGlobalGetResponse["results"]` type resolves to
* `{ [key: string]: unknown }` because the OpenAPI spec uses
* `additionalProperties`. This interface captures the actual shape so
* consumers get proper type safety.
*/
export interface GlobalSearchResponse {
conversations: Array<{
id: string;
title: string | null;
excerpt?: string;
updatedAt?: number;
matchCount?: number;
}>;
memories: Array<{
id: string;
kind: string;
text: string;
subject: string | null;
confidence: number;
updatedAt: number;
source: "lexical" | "semantic";
}>;
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
schedules: Array<{
id: string;
name: string;
expression?: string;
message?: string;
enabled?: boolean;
nextRunAt?: number | null;
}>;
contacts: Array<{
id: string;
displayName: string;
notes?: string | null;
lastInteraction?: number | null;
}>;
}
Comment thread
ashleeradka marked this conversation as resolved.

// ---------------------------------------------------------------------------
// API
Expand Down Expand Up @@ -48,7 +85,7 @@ export async function searchGlobal(
return EMPTY_RESULTS;
}

return data.results;
return data.results as unknown as GlobalSearchResponse;
} catch (err) {
// AbortError is expected when debounced queries supersede each other.
if (err instanceof DOMException && err.name === "AbortError") {
Expand Down
32 changes: 29 additions & 3 deletions apps/web/src/hooks/conversation-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ import {
assertHasResponse,
extractErrorMessage,
} from "@/utils/api-errors";
import { isLocalMode } from "@/lib/local-mode";
import { useAuthStore } from "@/stores/auth-store";
import { useOrganizationStore } from "@/stores/organization-store";
import {
archivedConversationsQueryKey,
conversationsQueryKey,
Expand Down Expand Up @@ -207,6 +210,26 @@ export async function listArchivedConversations(

const QUERY_STALE_TIME_MS = 30_000;

/**
* Platform-mode daemon requests require the `Vellum-Organization-Id`
* header, which the HeyAPI request interceptor reads from the org store
* (with a sessionStorage fallback). On a fresh session neither source
* has a value until `fetchOrganizations()` completes — firing the query
* before that produces a headerless request that Django rejects with 400.
*
* Returns `true` when:
* - local mode (gateway auth, no org header needed), or
* - no platform session (self-hosted/gateway-auth assistant — the
* interceptor rewrites to the user's gateway with Bearer auth and
* the org store intentionally stays empty), or
* - the org store has been populated.
*/
function useHasOrgContext(): boolean {
const currentOrgId = useOrganizationStore.use.currentOrganizationId();
const hasPlatformSession = useAuthStore.use.hasPlatformSession();
return isLocalMode() || !hasPlatformSession || currentOrgId != null;
Comment thread
ashleeradka marked this conversation as resolved.
Outdated
}

/**
* Subscribe to the conversation list for the given assistant.
*
Expand Down Expand Up @@ -236,10 +259,11 @@ export function useConversationListQuery(
error: Error | null;
refetch: () => void;
} {
const hasOrgContext = useHasOrgContext();
const query = useQuery({
queryKey: conversationsQueryKey(assistantId),
queryFn: () => listConversations(assistantId!),
enabled: enabled && Boolean(assistantId),
enabled: enabled && Boolean(assistantId) && hasOrgContext,
staleTime: QUERY_STALE_TIME_MS,
});
return {
Expand Down Expand Up @@ -274,10 +298,11 @@ export function useArchivedConversationListQuery(
error: Error | null;
refetch: () => void;
} {
const hasOrgContext = useHasOrgContext();
const query = useQuery({
queryKey: archivedConversationsQueryKey(assistantId),
queryFn: () => listArchivedConversations(assistantId!),
enabled: enabled && Boolean(assistantId),
enabled: enabled && Boolean(assistantId) && hasOrgContext,
staleTime: QUERY_STALE_TIME_MS,
});
return {
Expand All @@ -301,12 +326,13 @@ export function useConversationGroupsQuery(
assistantId: string | null,
enabled: boolean = true,
): { conversationGroups: ConversationGroup[]; isLoading: boolean } {
const hasOrgContext = useHasOrgContext();
const query = useQuery({
...groupsGetOptions({
path: { assistant_id: assistantId ?? "" },
} as Options<GroupsGetData>),
select: (data) => data.groups,
enabled: enabled && Boolean(assistantId),
enabled: enabled && Boolean(assistantId) && hasOrgContext,
staleTime: QUERY_STALE_TIME_MS,
});
return {
Expand Down