diff --git a/apps/web/src/components/composer/model-menu-controller.ts b/apps/web/src/components/composer/model-menu-controller.ts index fc207d75..f2dc773d 100644 --- a/apps/web/src/components/composer/model-menu-controller.ts +++ b/apps/web/src/components/composer/model-menu-controller.ts @@ -21,6 +21,7 @@ export interface ModelMenuController { displayOption: AgentModelOption; disabledModels: readonly string[]; isOpen: boolean; + isPolicyLoading: boolean; selectedOption: AgentModelOption; shouldRender: boolean; }; @@ -37,13 +38,14 @@ export function useModelMenuController({ }): ModelMenuController { const agentModelId = useAppStore((state) => state.agentModelId); const setAgentModelId = useAppStore((state) => state.setAgentModelId); - const disabledModels = useProfileQuery().data?.disabledModels ?? []; const menuRef = useRef(null); const menuId = `model-menu-${useId()}`; const [internalOpen, setInternalOpen] = useState(false); const isOpen = open ?? internalOpen; const setIsOpen = onOpenChange ?? setInternalOpen; const shouldRender = useMenuPresence(isOpen); + const profileQuery = useProfileQuery(shouldRender); + const disabledModels = profileQuery.data?.disabledModels ?? []; const selectedOption = agentModelOption(agentModelId); const resolvedOption = resolvedModelId ? agentModelOption(resolvedModelId) : null; const displayOption = @@ -61,7 +63,14 @@ export function useModelMenuController({ toggle: () => setIsOpen(!isOpen), }, meta: { menuId, menuRef }, - state: { displayOption, disabledModels, isOpen, selectedOption, shouldRender }, + state: { + displayOption, + disabledModels, + isOpen, + isPolicyLoading: shouldRender && profileQuery.isLoading, + selectedOption, + shouldRender, + }, }; } diff --git a/apps/web/src/components/composer/model-menu-view.tsx b/apps/web/src/components/composer/model-menu-view.tsx index 8667f00f..30057138 100644 --- a/apps/web/src/components/composer/model-menu-view.tsx +++ b/apps/web/src/components/composer/model-menu-view.tsx @@ -85,7 +85,9 @@ function ModelMenuOption({ controller: ModelMenuController; option: AgentModelOption; }) { - const isDisabled = option.id !== "auto" && controller.state.disabledModels.includes(option.id); + const isDisabled = + option.id !== "auto" && + (controller.state.isPolicyLoading || controller.state.disabledModels.includes(option.id)); const isActive = option.id === controller.state.selectedOption.id; const select = () => controller.actions.select(option.id); return ( @@ -102,7 +104,13 @@ function ModelMenuOption({ select(); }} role="menuitemradio" - title={isDisabled ? "Disabled in Models settings" : option.label} + title={ + controller.state.isPolicyLoading + ? "Loading model settings" + : isDisabled + ? "Disabled in Models settings" + : option.label + } type="button" > fetchComposerSkills(options.getToken, signal), - queryKey: COMPOSER_SKILLS_QUERY, - staleTime: 60_000, - }); const triggers = useComposerTriggers({ onChange: options.onChange, onInsert: (kind, item) => { @@ -59,6 +54,12 @@ export function useComposerMenu(options: UseComposerMenuOptions): ComposerMenuCo textareaRef: options.textareaRef, value: options.value, }); + const skillsCatalog = useQuery({ + enabled: triggers.kind === "mention", + queryFn: ({ signal }) => fetchComposerSkills(options.getToken, signal), + queryKey: COMPOSER_SKILLS_QUERY, + staleTime: 60_000, + }); const fileItems = useProjectFileItems({ enabled: triggers.kind === "slash", projectId: options.projectId, diff --git a/apps/web/src/components/home/home-computer-pane.tsx b/apps/web/src/components/home/home-computer-pane.tsx index f1df370f..9dbb95c2 100644 --- a/apps/web/src/components/home/home-computer-pane.tsx +++ b/apps/web/src/components/home/home-computer-pane.tsx @@ -89,7 +89,9 @@ function HomeComputerBody({ /> : null + activeTab === "files" ? ( + + ) : null } > diff --git a/apps/web/src/lib/hooks/use-profile.ts b/apps/web/src/lib/hooks/use-profile.ts index 7e741fc5..ea311d7d 100644 --- a/apps/web/src/lib/hooks/use-profile.ts +++ b/apps/web/src/lib/hooks/use-profile.ts @@ -8,10 +8,10 @@ import { getProfile, updateProfile } from "@/lib/api/profile"; const PROFILE_QUERY_KEY = ["me-profile"] as const; -export function useProfileQuery() { +export function useProfileQuery(enabled = true) { const { getToken, isSignedIn } = useAuth(); return useQuery({ - enabled: Boolean(isSignedIn), + enabled: Boolean(isSignedIn) && enabled, queryFn: ({ signal }) => getProfile(getToken, signal), queryKey: PROFILE_QUERY_KEY, staleTime: 30_000, diff --git a/packages/db/src/navigation-bootstrap.ts b/packages/db/src/navigation-bootstrap.ts index 3d0c0f58..deefc834 100644 --- a/packages/db/src/navigation-bootstrap.ts +++ b/packages/db/src/navigation-bootstrap.ts @@ -37,13 +37,26 @@ function listNavigationProjects( input: { activeThreadId?: ThreadId; userId: UserId }, ) { const activeProjectId = activeProjectIdExpression(input); + const latestThread = db + .select({ id: threads.id }) + .from(threads) + .where( + and( + eq(threads.projectId, projects.id), + eq(threads.userId, input.userId), + isNull(threads.deletedAt), + ), + ) + .orderBy(desc(threads.updatedAt), desc(threads.id)) + .limit(1) + .as("latest_thread"); return db .select({ activeProjectId, archiveAfter: projects.archiveAfter, createdAt: projects.createdAt, id: projects.id, - latestThreadId: latestThreadIdExpression(input.userId), + latestThreadId: latestThread.id, mode: projects.mode, name: projects.name, overQuota: projects.overQuota, @@ -52,6 +65,7 @@ function listNavigationProjects( workspaceSlug: projects.workspaceSlug, }) .from(projects) + .leftJoinLateral(latestThread, sql`true`) .where(and(eq(projects.userId, input.userId), isNull(projects.deletedAt))) .orderBy( sql`case when ${projects.id} = ${activeProjectId} then 0 else 1 end`, @@ -77,18 +91,6 @@ function activeProjectIdExpression(input: { activeThreadId?: ThreadId; userId: U )`; } -function latestThreadIdExpression(userId: UserId) { - return sql`( - select latest_thread.id - from ${threads} latest_thread - where latest_thread.project_id = ${projects.id} - and latest_thread.user_id = ${userId} - and latest_thread.deleted_at is null - order by latest_thread.updated_at desc, latest_thread.id desc - limit 1 - )`; -} - function navigationProjectFromRow( row: Awaited>[number], ): NavigationProjectRecord { diff --git a/packages/db/src/search.ts b/packages/db/src/search.ts index e422074c..be91f700 100644 --- a/packages/db/src/search.ts +++ b/packages/db/src/search.ts @@ -61,20 +61,28 @@ async function searchProjectRecords( pattern: string, limit: number, ): Promise { + const latestThread = db + .select({ id: threads.id }) + .from(threads) + .where( + and( + eq(threads.projectId, projects.id), + eq(threads.userId, userId), + isNull(threads.deletedAt), + ), + ) + .orderBy(desc(threads.updatedAt), desc(threads.id)) + .limit(1) + .as("latest_thread"); const projectRows = await db .select({ id: projects.id, name: projects.name, updatedAt: projects.updatedAt, - latestThreadId: sql`( - select sub.id - from ${threads} as sub - where sub.project_id = ${projects.id} and sub.deleted_at is null - order by sub.updated_at desc - limit 1 - )`, + latestThreadId: latestThread.id, }) .from(projects) + .leftJoinLateral(latestThread, sql`true`) .where( and( eq(projects.userId, userId),