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
50 changes: 13 additions & 37 deletions ui/goose2/src/app/hooks/useAppStartup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,40 +75,6 @@ export function useAppStartup() {
}
};

const refreshConfiguredProviderInventory = async (
initialEntries?: Awaited<ReturnType<typeof loadProvidersAndInventory>>,
) => {
try {
const entries =
initialEntries && initialEntries.length > 0
? initialEntries
: await (async () => {
const { getProviderInventory } = await import(
"@/features/providers/api/inventory"
);
return getProviderInventory();
})();
const configuredProviderIds = entries
.filter((entry) => entry.configured)
.map((entry) => entry.providerId);
if (configuredProviderIds.length === 0) {
return;
}

const { syncProviderInventory } = await import(
"@/features/providers/api/inventorySync"
);
await syncProviderInventory(configuredProviderIds, {
onEntries: (entries) => inventoryStore.mergeEntries(entries),
});
} catch (err) {
console.error(
"Failed to refresh provider inventory on startup:",
err,
);
}
};

const loadSessionState = async () => {
const t0 = performance.now();
perfLog("[perf:startup] loadSessionState start");
Expand All @@ -128,9 +94,19 @@ export function useAppStartup() {
providersAndInventoryLoad,
loadSessionState(),
]);
void providersAndInventoryLoad.then((entries) =>
refreshConfiguredProviderInventory(entries),
);
void providersAndInventoryLoad.then(async (entries) => {
try {
const { backgroundRefreshInventory } = await import(
"@/features/providers/api/inventory"
);
await backgroundRefreshInventory(inventoryStore, entries);
} catch (err) {
console.error(
"Failed to refresh provider inventory on startup:",
err,
);
}
});
perfLog(
`[perf:startup] useAppStartup complete in ${(performance.now() - tStartup).toFixed(1)}ms`,
);
Expand Down
22 changes: 21 additions & 1 deletion ui/goose2/src/features/chat/hooks/useAgentModelPickerState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useMemo } from "react";
import { useCallback, useMemo, useRef } from "react";
import type { AcpProvider } from "@/shared/api/acp";
import { useProviderInventory } from "@/features/providers/hooks/useProviderInventory";
import { useProviderInventoryStore } from "@/features/providers/stores/providerInventoryStore";
import {
getCatalogEntry,
resolveAgentProviderCatalogIdStrict,
Expand Down Expand Up @@ -162,6 +163,24 @@ export function useAgentModelPickerState({
[availableModels, onModelSelected],
);

const refreshingRef = useRef(false);
const handlePickerOpen = useCallback(() => {
if (refreshingRef.current || useProviderInventoryStore.getState().loading) {
return;
}
refreshingRef.current = true;
import("@/features/providers/api/inventory")
.then(({ backgroundRefreshInventory }) =>
backgroundRefreshInventory(useProviderInventoryStore.getState()),
)
.catch((err) =>
console.error("Failed to background-refresh inventory:", err),
)
.finally(() => {
refreshingRef.current = false;
});
}, []);

return {
selectedAgentId,
pickerAgents,
Expand All @@ -170,5 +189,6 @@ export function useAgentModelPickerState({
modelStatusMessage,
handleProviderChange,
handleModelChange,
handlePickerOpen,
};
}
2 changes: 2 additions & 0 deletions ui/goose2/src/features/chat/hooks/useChatSessionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ export function useChatSessionController({
modelStatusMessage,
handleProviderChange,
handleModelChange,
handlePickerOpen,
effectiveModelSelection,
} = useResolvedAgentModelPicker({
providers,
Expand Down Expand Up @@ -819,6 +820,7 @@ export function useChatSessionController({
modelsLoading,
modelStatusMessage,
handleModelChange: handleModelChangeWithContextReset,
handlePickerOpen,
selectedProjectId: effectiveProjectId,
availableProjects,
handleProjectChange,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export function useResolvedAgentModelPicker({
modelStatusMessage,
handleProviderChange,
handleModelChange,
handlePickerOpen,
} = useAgentModelPickerState({
providers,
selectedProvider,
Expand Down Expand Up @@ -463,6 +464,7 @@ export function useResolvedAgentModelPicker({
modelStatusMessage,
handleProviderChange,
handleModelChange,
handlePickerOpen,
effectiveModelSelection,
};
}
1 change: 1 addition & 0 deletions ui/goose2/src/features/chat/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export interface ChatInputProps {
modelsLoading?: boolean;
modelStatusMessage?: string | null;
onModelChange?: (modelId: string) => void;
onPickerOpen?: () => void;
selectedProjectId?: string | null;
availableProjects?: ProjectOption[];
onProjectChange?: (projectId: string | null) => void;
Expand Down
72 changes: 32 additions & 40 deletions ui/goose2/src/features/chat/ui/AgentModelPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import {
} from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import type { AcpProvider } from "@/shared/api/acp";
import { getProviderInventory } from "@/features/providers/api/inventory";
import { useProviderInventoryStore } from "@/features/providers/stores/providerInventoryStore";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
Expand All @@ -34,6 +32,7 @@ interface AgentModelPickerProps {
loading?: boolean;
isCompact?: boolean;
showSelectedModelInTrigger?: boolean;
onOpen?: () => void;
}

function getModelDisplayName(model: ModelOption) {
Expand Down Expand Up @@ -321,14 +320,11 @@ export function AgentModelPicker({
loading = false,
isCompact = false,
showSelectedModelInTrigger = true,
onOpen,
}: AgentModelPickerProps) {
const { t } = useTranslation("chat");
const [open, setOpen] = useState(false);
const [modelView, setModelView] = useState<ModelView>("recommended");
const mergeInventoryEntries = useProviderInventoryStore(
(s) => s.mergeEntries,
);

const selectedAgentLabel =
agents.find((agent) => agent.id === selectedAgentId)?.label ??
formatProviderLabel(selectedAgentId);
Expand Down Expand Up @@ -358,52 +354,32 @@ export function AgentModelPicker({
}
}, [open]);
Comment thread
matt2e marked this conversation as resolved.

useEffect(() => {
if (!open) {
return;
}

let cancelled = false;

const syncInventory = async () => {
try {
const entries = await getProviderInventory();
if (cancelled) {
return;
}
mergeInventoryEntries(entries);
} catch (error) {
console.error("Failed to sync provider inventory from picker:", error);
}
};

void syncInventory();

return () => {
cancelled = true;
};
}, [open, mergeInventoryEntries]);

// When in "all" view, expand the popover to full width for the search experience.
const isAllView = modelView === "all";

return (
<Popover open={open} onOpenChange={setOpen}>
<Popover
open={open}
onOpenChange={(nextOpen) => {
setOpen(nextOpen);
if (nextOpen) onOpen?.();
}}
>
<PopoverTrigger asChild>
<Button
type="button"
variant="toolbar"
size="sm"
aria-label={t("toolbar.chooseAgentModel")}
disabled={loading}
disabled={loading && !selectedAgentLabel}
leftIcon={getProviderIcon(selectedAgentId, "size-3.5")}
rightIcon={<IconChevronDown className="opacity-50" />}
className="min-w-0"
>
<span className={cn("truncate", isCompact ? "max-w-32" : "max-w-56")}>
{loading
? t("toolbar.loading")
: (triggerModelLabel ?? selectedAgentLabel)}
{triggerModelLabel ??
selectedAgentLabel ??
(loading ? t("toolbar.loading") : null)}
</span>
</Button>
</PopoverTrigger>
Expand Down Expand Up @@ -512,9 +488,25 @@ export function AgentModelPicker({
className="flex min-h-0 min-w-0 overflow-hidden p-1"
>
{modelsLoading ? (
<div className="flex min-h-0 flex-1 items-center gap-2 px-2 py-2 text-sm text-muted-foreground">
<Spinner className="size-4" />
<span>{t("toolbar.loadingModels")}</span>
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<div className="shrink-0 px-2 py-1.5 text-sm font-semibold">
{t("toolbar.model")}
</div>
{currentModelName || currentModelId ? (
<div className="space-y-0.5 p-1">
<PickerItem selected disabled>
<div className="min-w-0 flex-1 truncate">
{currentModelName ?? currentModelId}
</div>
<Spinner className="size-3.5 shrink-0" />
</PickerItem>
</div>
) : (
<div className="flex min-h-0 flex-1 items-center gap-2 px-2 py-2 text-sm text-muted-foreground">
<Spinner className="size-4" />
<span>{t("toolbar.loadingModels")}</span>
</div>
)}
</div>
) : availableModels.length > 0 ? (
modelView === "recommended" ? (
Expand Down
2 changes: 2 additions & 0 deletions ui/goose2/src/features/chat/ui/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function ChatInput({
modelsLoading = false,
modelStatusMessage = null,
onModelChange,
onPickerOpen,
selectedProjectId = null,
availableProjects = [],
onProjectChange,
Expand Down Expand Up @@ -455,6 +456,7 @@ export function ChatInput({
modelsLoading={modelsLoading}
modelStatusMessage={modelStatusMessage}
onModelChange={onModelChange}
onPickerOpen={onPickerOpen}
selectedProjectId={selectedProjectId}
availableProjects={availableProjects}
onProjectChange={onProjectChange}
Expand Down
3 changes: 3 additions & 0 deletions ui/goose2/src/features/chat/ui/ChatInputToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ interface ChatInputToolbarProps {
modelsLoading?: boolean;
modelStatusMessage?: string | null;
onModelChange?: (modelId: string) => void;
onPickerOpen?: () => void;
// Project
selectedProjectId: string | null;
availableProjects: ProjectOption[];
Expand Down Expand Up @@ -108,6 +109,7 @@ export function ChatInputToolbar({
modelsLoading = false,
modelStatusMessage = null,
onModelChange,
onPickerOpen,
selectedProjectId,
availableProjects,
onProjectChange,
Expand Down Expand Up @@ -231,6 +233,7 @@ export function ChatInputToolbar({
modelsLoading={modelsLoading}
modelStatusMessage={modelStatusMessage}
onModelChange={onModelChange}
onOpen={onPickerOpen}
loading={providersLoading}
isCompact={isCompact}
showSelectedModelInTrigger={selectedPersonaId === null}
Expand Down
1 change: 1 addition & 0 deletions ui/goose2/src/features/chat/ui/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export function ChatView({
modelsLoading={controller.modelsLoading}
modelStatusMessage={controller.modelStatusMessage}
onModelChange={controller.handleModelChange}
onPickerOpen={controller.handlePickerOpen}
selectedProjectId={controller.selectedProjectId}
availableProjects={controller.availableProjects}
onProjectChange={controller.handleProjectChange}
Expand Down
1 change: 1 addition & 0 deletions ui/goose2/src/features/home/ui/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ function HomeComposer({
modelsLoading={controller.modelsLoading}
modelStatusMessage={controller.modelStatusMessage}
onModelChange={controller.handleModelChange}
onPickerOpen={controller.handlePickerOpen}
selectedProjectId={controller.selectedProjectId}
availableProjects={controller.availableProjects}
onProjectChange={controller.handleProjectChange}
Expand Down
Loading
Loading