mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix(site): hoist model queries out of AgentDetail (#23324)
This commit is contained in:
@@ -51,6 +51,49 @@ const AgentDetailLayout: FC = () => {
|
||||
requestUnarchiveAgent: () => {},
|
||||
isSidebarCollapsed: false,
|
||||
onToggleSidebarCollapsed: () => {},
|
||||
modelOptions: [
|
||||
{
|
||||
id: "openai:gpt-4o",
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
displayName: "GPT-4o",
|
||||
},
|
||||
],
|
||||
modelConfigIDByModelID: new Map([["openai:gpt-4o", "config-1"]]),
|
||||
modelIDByConfigID: new Map([["config-1", "openai:gpt-4o"]]),
|
||||
modelConfigs: [
|
||||
{
|
||||
id: "config-1",
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
display_name: "GPT-4o",
|
||||
enabled: true,
|
||||
is_default: false,
|
||||
context_limit: 200000,
|
||||
compression_threshold: 70,
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
updated_at: "2026-01-01T00:00:00Z",
|
||||
},
|
||||
],
|
||||
modelCatalog: {
|
||||
providers: [
|
||||
{
|
||||
provider: "openai",
|
||||
available: true,
|
||||
models: [
|
||||
{
|
||||
id: "openai:gpt-4o",
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
display_name: "GPT-4o",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
isModelCatalogLoading: false,
|
||||
modelCatalogError: null,
|
||||
desktopEnabled: false,
|
||||
} satisfies AgentsOutletContext
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -2,10 +2,7 @@ import { API, watchWorkspace } from "api/api";
|
||||
import { isApiError } from "api/errors";
|
||||
import {
|
||||
chat,
|
||||
chatDesktopEnabled,
|
||||
chatMessagesForInfiniteScroll,
|
||||
chatModelConfigs,
|
||||
chatModels,
|
||||
chats,
|
||||
createChatMessage,
|
||||
deleteChatQueuedMessage,
|
||||
@@ -55,9 +52,7 @@ import {
|
||||
import type { AgentsOutletContext } from "./AgentsPage";
|
||||
import {
|
||||
getModelCatalogStatusMessage,
|
||||
getModelOptionsFromCatalog,
|
||||
getModelSelectorPlaceholder,
|
||||
getNormalizedModelRef,
|
||||
hasConfiguredModelsInCatalog,
|
||||
} from "./modelOptions";
|
||||
import { parsePullRequestUrl } from "./pullRequest";
|
||||
@@ -258,6 +253,14 @@ const AgentDetail: FC = () => {
|
||||
onOpenAnalytics,
|
||||
isSidebarCollapsed,
|
||||
onToggleSidebarCollapsed,
|
||||
modelOptions,
|
||||
modelConfigIDByModelID,
|
||||
modelIDByConfigID,
|
||||
modelConfigs,
|
||||
modelCatalog,
|
||||
isModelCatalogLoading,
|
||||
modelCatalogError,
|
||||
desktopEnabled,
|
||||
} = outletContext;
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const chatInputRef = useRef<ChatMessageInputRef | null>(null);
|
||||
@@ -318,9 +321,6 @@ const AgentDetail: FC = () => {
|
||||
});
|
||||
return () => socket.close();
|
||||
}, [workspaceId, queryClient]);
|
||||
const chatModelsQuery = useQuery(chatModels());
|
||||
const chatModelConfigsQuery = useQuery(chatModelConfigs());
|
||||
const desktopEnabledQuery = useQuery(chatDesktopEnabled());
|
||||
const sshConfigQuery = useQuery(deploymentSSHConfig());
|
||||
const workspace = workspaceQuery.data;
|
||||
const workspaceAgent = getWorkspaceAgent(workspace, undefined);
|
||||
@@ -385,42 +385,6 @@ const AgentDetail: FC = () => {
|
||||
const isArchived = chatRecord?.archived ?? false;
|
||||
const chatLastModelConfigID = chatRecord?.last_model_config_id;
|
||||
|
||||
const modelOptions = useMemo(
|
||||
() =>
|
||||
getModelOptionsFromCatalog(
|
||||
chatModelsQuery.data,
|
||||
chatModelConfigsQuery.data,
|
||||
),
|
||||
[chatModelsQuery.data, chatModelConfigsQuery.data],
|
||||
);
|
||||
const modelConfigIDByModelID = useMemo(() => {
|
||||
const byModelID = new Map<string, string>();
|
||||
for (const config of chatModelConfigsQuery.data ?? []) {
|
||||
const { provider, model } = getNormalizedModelRef(config);
|
||||
if (!provider || !model) {
|
||||
continue;
|
||||
}
|
||||
const colonRef = `${provider}:${model}`;
|
||||
if (!byModelID.has(colonRef)) {
|
||||
byModelID.set(colonRef, config.id);
|
||||
}
|
||||
const slashRef = `${provider}/${model}`;
|
||||
if (!byModelID.has(slashRef)) {
|
||||
byModelID.set(slashRef, config.id);
|
||||
}
|
||||
}
|
||||
return byModelID;
|
||||
}, [chatModelConfigsQuery.data]);
|
||||
const modelIDByConfigID = useMemo(() => {
|
||||
const byConfigID = new Map<string, string>();
|
||||
for (const [modelID, configID] of modelConfigIDByModelID.entries()) {
|
||||
if (!byConfigID.has(configID)) {
|
||||
byConfigID.set(configID, modelID);
|
||||
}
|
||||
}
|
||||
return byConfigID;
|
||||
}, [modelConfigIDByModelID]);
|
||||
|
||||
const sendMutation = useMutation(
|
||||
createChatMessage(queryClient, agentId ?? ""),
|
||||
);
|
||||
@@ -501,25 +465,21 @@ const AgentDetail: FC = () => {
|
||||
if (!chatLastModelConfigID) {
|
||||
return undefined;
|
||||
}
|
||||
const config = chatModelConfigsQuery.data?.find(
|
||||
(c) => c.id === chatLastModelConfigID,
|
||||
);
|
||||
const config = modelConfigs.find((c) => c.id === chatLastModelConfigID);
|
||||
return config?.compression_threshold;
|
||||
}, [chatLastModelConfigID, chatModelConfigsQuery.data]);
|
||||
}, [chatLastModelConfigID, modelConfigs]);
|
||||
const hasModelOptions = modelOptions.length > 0;
|
||||
const hasConfiguredModels = hasConfiguredModelsInCatalog(
|
||||
chatModelsQuery.data,
|
||||
);
|
||||
const hasConfiguredModels = hasConfiguredModelsInCatalog(modelCatalog);
|
||||
const modelSelectorPlaceholder = getModelSelectorPlaceholder(
|
||||
modelOptions,
|
||||
chatModelsQuery.isLoading,
|
||||
isModelCatalogLoading,
|
||||
hasConfiguredModels,
|
||||
);
|
||||
const modelCatalogStatusMessage = getModelCatalogStatusMessage(
|
||||
chatModelsQuery.data,
|
||||
modelCatalog,
|
||||
modelOptions,
|
||||
chatModelsQuery.isLoading,
|
||||
Boolean(chatModelsQuery.error),
|
||||
isModelCatalogLoading,
|
||||
Boolean(modelCatalogError),
|
||||
);
|
||||
const inputStatusText = hasModelOptions
|
||||
? null
|
||||
@@ -913,9 +873,7 @@ const AgentDetail: FC = () => {
|
||||
hasMoreMessages={chatMessagesQuery.hasNextPage ?? false}
|
||||
isFetchingMoreMessages={chatMessagesQuery.isFetchingNextPage}
|
||||
onFetchMoreMessages={chatMessagesQuery.fetchNextPage}
|
||||
desktopChatId={
|
||||
desktopEnabledQuery.data?.enable_desktop ? agentId : undefined
|
||||
}
|
||||
desktopChatId={desktopEnabled ? agentId : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -130,6 +130,14 @@ const AgentEmbedPage: FC = () => {
|
||||
requestArchiveAndDeleteWorkspace,
|
||||
isSidebarCollapsed,
|
||||
onToggleSidebarCollapsed,
|
||||
modelOptions: [],
|
||||
modelConfigIDByModelID: new Map(),
|
||||
modelIDByConfigID: new Map(),
|
||||
modelConfigs: [],
|
||||
modelCatalog: undefined,
|
||||
isModelCatalogLoading: false,
|
||||
modelCatalogError: null,
|
||||
desktopEnabled: false,
|
||||
}),
|
||||
[
|
||||
chatErrorReasons,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { API, watchChats } from "api/api";
|
||||
import { getErrorMessage } from "api/errors";
|
||||
import {
|
||||
archiveChat,
|
||||
chatDesktopEnabled,
|
||||
chatDiffContentsKey,
|
||||
chatKey,
|
||||
chatModelConfigs,
|
||||
@@ -41,7 +42,6 @@ import {
|
||||
emptyInputStorageKey,
|
||||
} from "./AgentCreateForm";
|
||||
import { maybePlayChime } from "./AgentDetail/useAgentChime";
|
||||
import type { AgentsOutletContext } from "./AgentsPageView";
|
||||
import { AgentsPageView } from "./AgentsPageView";
|
||||
import { resolveArchiveAndDeleteAction } from "./agentWorkspaceUtils";
|
||||
import {
|
||||
@@ -134,6 +134,7 @@ const AgentsPage: FC = () => {
|
||||
);
|
||||
const chatModelsQuery = useQuery(chatModels());
|
||||
const chatModelConfigsQuery = useQuery(chatModelConfigs());
|
||||
const desktopEnabledQuery = useQuery(chatDesktopEnabled());
|
||||
const createMutation = useMutation(createChat(queryClient));
|
||||
const archiveChatBase = archiveChat(queryClient);
|
||||
const archiveAgentMutation = useMutation({
|
||||
@@ -319,28 +320,6 @@ const AgentsPage: FC = () => {
|
||||
() => setIsSidebarCollapsed((prev) => !prev),
|
||||
[],
|
||||
);
|
||||
const outletContext: AgentsOutletContext = useMemo(
|
||||
() => ({
|
||||
chatErrorReasons,
|
||||
setChatErrorReason,
|
||||
clearChatErrorReason,
|
||||
requestArchiveAgent,
|
||||
requestUnarchiveAgent,
|
||||
requestArchiveAndDeleteWorkspace,
|
||||
isSidebarCollapsed,
|
||||
onToggleSidebarCollapsed: handleToggleSidebarCollapsed,
|
||||
}),
|
||||
[
|
||||
chatErrorReasons,
|
||||
setChatErrorReason,
|
||||
clearChatErrorReason,
|
||||
requestArchiveAgent,
|
||||
requestUnarchiveAgent,
|
||||
requestArchiveAndDeleteWorkspace,
|
||||
isSidebarCollapsed,
|
||||
handleToggleSidebarCollapsed,
|
||||
],
|
||||
);
|
||||
const handleCreateChat = async (options: CreateChatOptions) => {
|
||||
const { message, fileIDs, workspaceId, model } = options;
|
||||
const modelConfigID =
|
||||
@@ -555,13 +534,21 @@ const AgentsPage: FC = () => {
|
||||
onCollapseSidebar={() => setIsSidebarCollapsed(true)}
|
||||
isSidebarCollapsed={isSidebarCollapsed}
|
||||
onExpandSidebar={() => setIsSidebarCollapsed(false)}
|
||||
outletContext={outletContext}
|
||||
chatErrorReasons={chatErrorReasons}
|
||||
setChatErrorReason={setChatErrorReason}
|
||||
clearChatErrorReason={clearChatErrorReason}
|
||||
requestArchiveAgent={requestArchiveAgent}
|
||||
requestUnarchiveAgent={requestUnarchiveAgent}
|
||||
requestArchiveAndDeleteWorkspace={requestArchiveAndDeleteWorkspace}
|
||||
onToggleSidebarCollapsed={handleToggleSidebarCollapsed}
|
||||
onCreateChat={handleCreateChat}
|
||||
createError={createMutation.error}
|
||||
modelCatalog={chatModelsQuery.data}
|
||||
isModelCatalogLoading={chatModelsQuery.isLoading}
|
||||
isModelConfigsLoading={chatModelConfigsQuery.isLoading}
|
||||
modelCatalogError={chatModelsQuery.error}
|
||||
modelConfigIDByModelID={modelConfigIDByModelID}
|
||||
desktopEnabled={desktopEnabledQuery.data?.enable_desktop ?? false}
|
||||
isAgentsAdmin={isAgentsAdmin}
|
||||
hasNextPage={chatsQuery.hasNextPage}
|
||||
onLoadMore={() => void chatsQuery.fetchNextPage()}
|
||||
@@ -584,5 +571,4 @@ const AgentsPage: FC = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentsPage;
|
||||
|
||||
@@ -162,16 +162,13 @@ const meta: Meta<typeof AgentsPageView> = {
|
||||
onCollapseSidebar: fn(),
|
||||
isSidebarCollapsed: false,
|
||||
onExpandSidebar: fn(),
|
||||
outletContext: {
|
||||
chatErrorReasons: {},
|
||||
setChatErrorReason: fn(),
|
||||
clearChatErrorReason: fn(),
|
||||
requestArchiveAgent: fn(),
|
||||
requestUnarchiveAgent: fn(),
|
||||
requestArchiveAndDeleteWorkspace: fn(),
|
||||
isSidebarCollapsed: false,
|
||||
onToggleSidebarCollapsed: fn(),
|
||||
},
|
||||
chatErrorReasons: {},
|
||||
setChatErrorReason: fn(),
|
||||
clearChatErrorReason: fn(),
|
||||
requestArchiveAgent: fn(),
|
||||
requestUnarchiveAgent: fn(),
|
||||
requestArchiveAndDeleteWorkspace: fn(),
|
||||
onToggleSidebarCollapsed: fn(),
|
||||
isAgentsAdmin: false,
|
||||
analyticsNow: fixedAnalyticsNow,
|
||||
archivedFilter: "active" as const,
|
||||
@@ -181,10 +178,27 @@ const meta: Meta<typeof AgentsPageView> = {
|
||||
isFetchingNextPage: false,
|
||||
onCreateChat: fn(),
|
||||
createError: undefined,
|
||||
modelCatalog: undefined,
|
||||
modelCatalog: {
|
||||
providers: [
|
||||
{
|
||||
provider: "openai",
|
||||
available: true,
|
||||
models: [
|
||||
{
|
||||
id: "openai:gpt-4o",
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
display_name: "GPT-4o",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
isModelCatalogLoading: false,
|
||||
isModelConfigsLoading: false,
|
||||
modelCatalogError: undefined,
|
||||
modelConfigIDByModelID: new Map(),
|
||||
desktopEnabled: false,
|
||||
},
|
||||
beforeEach: () => {
|
||||
spyOn(API, "getWorkspaces").mockResolvedValue({
|
||||
@@ -278,16 +292,13 @@ export const SidebarCollapsed: Story = {
|
||||
updated_at: todayTimestamp,
|
||||
}),
|
||||
],
|
||||
outletContext: {
|
||||
chatErrorReasons: {},
|
||||
setChatErrorReason: fn(),
|
||||
clearChatErrorReason: fn(),
|
||||
requestArchiveAgent: fn(),
|
||||
requestUnarchiveAgent: fn(),
|
||||
requestArchiveAndDeleteWorkspace: fn(),
|
||||
isSidebarCollapsed: true,
|
||||
onToggleSidebarCollapsed: fn(),
|
||||
},
|
||||
chatErrorReasons: {},
|
||||
setChatErrorReason: fn(),
|
||||
clearChatErrorReason: fn(),
|
||||
requestArchiveAgent: fn(),
|
||||
requestUnarchiveAgent: fn(),
|
||||
requestArchiveAndDeleteWorkspace: fn(),
|
||||
onToggleSidebarCollapsed: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -434,19 +445,16 @@ export const WithErrorReasons: Story = {
|
||||
updated_at: todayTimestamp,
|
||||
}),
|
||||
],
|
||||
outletContext: {
|
||||
chatErrorReasons: {
|
||||
"chat-1": { kind: "generic", message: "Model rate limited" },
|
||||
"chat-3": { kind: "generic", message: "Context window exceeded" },
|
||||
},
|
||||
setChatErrorReason: fn(),
|
||||
clearChatErrorReason: fn(),
|
||||
requestArchiveAgent: fn(),
|
||||
requestUnarchiveAgent: fn(),
|
||||
requestArchiveAndDeleteWorkspace: fn(),
|
||||
isSidebarCollapsed: false,
|
||||
onToggleSidebarCollapsed: fn(),
|
||||
chatErrorReasons: {
|
||||
"chat-1": { kind: "generic", message: "Model rate limited" },
|
||||
"chat-3": { kind: "generic", message: "Context window exceeded" },
|
||||
},
|
||||
setChatErrorReason: fn(),
|
||||
clearChatErrorReason: fn(),
|
||||
requestArchiveAgent: fn(),
|
||||
requestUnarchiveAgent: fn(),
|
||||
requestArchiveAndDeleteWorkspace: fn(),
|
||||
onToggleSidebarCollapsed: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -29,9 +29,17 @@ export interface AgentsOutletContext {
|
||||
chatId: string,
|
||||
workspaceId: string,
|
||||
) => void;
|
||||
onOpenAnalytics?: () => void;
|
||||
isSidebarCollapsed: boolean;
|
||||
onToggleSidebarCollapsed: () => void;
|
||||
onOpenAnalytics?: () => void;
|
||||
modelOptions: readonly ModelSelectorOption[];
|
||||
modelConfigIDByModelID: ReadonlyMap<string, string>;
|
||||
modelIDByConfigID: ReadonlyMap<string, string>;
|
||||
modelConfigs: readonly TypesGen.ChatModelConfig[];
|
||||
modelCatalog: TypesGen.ChatModelsResponse | null | undefined;
|
||||
isModelCatalogLoading: boolean;
|
||||
modelCatalogError: unknown;
|
||||
desktopEnabled: boolean;
|
||||
}
|
||||
|
||||
interface AgentsPageViewProps {
|
||||
@@ -50,7 +58,16 @@ interface AgentsPageViewProps {
|
||||
onCollapseSidebar: () => void;
|
||||
isSidebarCollapsed: boolean;
|
||||
onExpandSidebar: () => void;
|
||||
outletContext: AgentsOutletContext;
|
||||
chatErrorReasons: Record<string, ChatDetailError>;
|
||||
setChatErrorReason: (chatId: string, reason: ChatDetailError) => void;
|
||||
clearChatErrorReason: (chatId: string) => void;
|
||||
requestArchiveAgent: (chatId: string) => void;
|
||||
requestUnarchiveAgent: (chatId: string) => void;
|
||||
requestArchiveAndDeleteWorkspace: (
|
||||
chatId: string,
|
||||
workspaceId: string,
|
||||
) => void;
|
||||
onToggleSidebarCollapsed: () => void;
|
||||
isAgentsAdmin: boolean;
|
||||
onCreateChat: (options: CreateChatOptions) => Promise<void>;
|
||||
createError: unknown;
|
||||
@@ -58,6 +75,8 @@ interface AgentsPageViewProps {
|
||||
isModelCatalogLoading: boolean;
|
||||
isModelConfigsLoading: boolean;
|
||||
modelCatalogError: unknown;
|
||||
modelConfigIDByModelID: ReadonlyMap<string, string>;
|
||||
desktopEnabled: boolean;
|
||||
hasNextPage: boolean | undefined;
|
||||
onLoadMore: () => void;
|
||||
isFetchingNextPage: boolean;
|
||||
@@ -82,7 +101,13 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
|
||||
onCollapseSidebar,
|
||||
isSidebarCollapsed,
|
||||
onExpandSidebar,
|
||||
outletContext,
|
||||
chatErrorReasons,
|
||||
setChatErrorReason,
|
||||
clearChatErrorReason,
|
||||
requestArchiveAgent,
|
||||
requestUnarchiveAgent,
|
||||
requestArchiveAndDeleteWorkspace,
|
||||
onToggleSidebarCollapsed,
|
||||
isAgentsAdmin,
|
||||
onCreateChat,
|
||||
createError,
|
||||
@@ -90,6 +115,8 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
|
||||
isModelCatalogLoading,
|
||||
isModelConfigsLoading,
|
||||
modelCatalogError,
|
||||
modelConfigIDByModelID,
|
||||
desktopEnabled,
|
||||
hasNextPage,
|
||||
onLoadMore,
|
||||
isFetchingNextPage,
|
||||
@@ -97,12 +124,6 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
|
||||
onArchivedFilterChange,
|
||||
analyticsNow,
|
||||
}) => {
|
||||
const {
|
||||
chatErrorReasons,
|
||||
requestArchiveAgent,
|
||||
requestUnarchiveAgent,
|
||||
requestArchiveAndDeleteWorkspace,
|
||||
} = outletContext;
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const sidebarView = sidebarViewFromPath(location.pathname);
|
||||
@@ -124,9 +145,55 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
|
||||
[chatErrorReasons],
|
||||
);
|
||||
|
||||
const outletContextValue = useMemo(
|
||||
() => ({ ...outletContext, onOpenAnalytics: handleOpenAnalytics }),
|
||||
[outletContext, handleOpenAnalytics],
|
||||
const modelIDByConfigID = useMemo(() => {
|
||||
const byConfigID = new Map<string, string>();
|
||||
for (const [modelID, configID] of modelConfigIDByModelID.entries()) {
|
||||
if (!byConfigID.has(configID)) {
|
||||
byConfigID.set(configID, modelID);
|
||||
}
|
||||
}
|
||||
return byConfigID;
|
||||
}, [modelConfigIDByModelID]);
|
||||
|
||||
const outletContextValue: AgentsOutletContext = useMemo(
|
||||
() => ({
|
||||
chatErrorReasons,
|
||||
setChatErrorReason,
|
||||
clearChatErrorReason,
|
||||
requestArchiveAgent,
|
||||
requestUnarchiveAgent,
|
||||
requestArchiveAndDeleteWorkspace,
|
||||
isSidebarCollapsed,
|
||||
onToggleSidebarCollapsed,
|
||||
onOpenAnalytics: handleOpenAnalytics,
|
||||
modelOptions: catalogModelOptions,
|
||||
modelConfigIDByModelID,
|
||||
modelIDByConfigID,
|
||||
modelConfigs,
|
||||
modelCatalog,
|
||||
isModelCatalogLoading,
|
||||
modelCatalogError,
|
||||
desktopEnabled,
|
||||
}),
|
||||
[
|
||||
chatErrorReasons,
|
||||
setChatErrorReason,
|
||||
clearChatErrorReason,
|
||||
requestArchiveAgent,
|
||||
requestUnarchiveAgent,
|
||||
requestArchiveAndDeleteWorkspace,
|
||||
isSidebarCollapsed,
|
||||
onToggleSidebarCollapsed,
|
||||
handleOpenAnalytics,
|
||||
catalogModelOptions,
|
||||
modelConfigIDByModelID,
|
||||
modelIDByConfigID,
|
||||
modelConfigs,
|
||||
modelCatalog,
|
||||
isModelCatalogLoading,
|
||||
modelCatalogError,
|
||||
desktopEnabled,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user