refactor(site): restructure agents routing and directory layout (#23408)

This commit is contained in:
Danielle Maywood
2026-03-22 23:58:58 +00:00
committed by GitHub
parent b763b72b53
commit b87171086c
135 changed files with 532 additions and 457 deletions
@@ -0,0 +1,50 @@
import { chatCostSummary } from "api/queries/chats";
import { useAuthContext } from "contexts/auth/AuthProvider";
import dayjs, { type Dayjs } from "dayjs";
import type { FC } from "react";
import { useQuery } from "react-query";
import { AgentAnalyticsPageView } from "./AgentAnalyticsPageView";
import { AgentPageHeader } from "./components/AgentPageHeader";
const createDateRange = (now?: Dayjs) => {
const end = now ?? dayjs();
const start = end.subtract(30, "day");
return {
startDate: start.toISOString(),
endDate: end.toISOString(),
rangeLabel: `${start.format("MMM D")} ${end.format("MMM D, YYYY")}`,
};
};
interface AgentAnalyticsPageProps {
/** Override the current time for deterministic storybook snapshots. */
now?: Dayjs;
}
const AgentAnalyticsPage: FC<AgentAnalyticsPageProps> = ({ now }) => {
const { user } = useAuthContext();
const dateRange = createDateRange(now);
const summaryQuery = useQuery({
...chatCostSummary(user?.id ?? "me", {
start_date: dateRange.startDate,
end_date: dateRange.endDate,
}),
enabled: Boolean(user?.id),
});
return (
<>
<AgentPageHeader />
<AgentAnalyticsPageView
summary={summaryQuery.data}
isLoading={summaryQuery.isLoading}
error={summaryQuery.error}
onRetry={() => void summaryQuery.refetch()}
rangeLabel={dateRange.rangeLabel}
/>
</>
);
};
export default AgentAnalyticsPage;
@@ -0,0 +1,47 @@
import type { ChatCostSummary } from "api/typesGenerated";
import { BarChart3Icon } from "lucide-react";
import type { FC } from "react";
import { ChatCostSummaryView } from "./components/ChatCostSummaryView";
import { SectionHeader } from "./components/SectionHeader";
interface AgentAnalyticsPageViewProps {
summary: ChatCostSummary | undefined;
isLoading: boolean;
error: unknown;
onRetry: () => void;
rangeLabel: string;
}
export const AgentAnalyticsPageView: FC<AgentAnalyticsPageViewProps> = ({
summary,
isLoading,
error,
onRetry,
rangeLabel,
}) => {
return (
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto p-4 pt-8 [scrollbar-width:thin] [scrollbar-color:hsl(var(--surface-quaternary))_transparent]">
<div className="mx-auto w-full max-w-3xl">
<SectionHeader
label="Analytics"
description="Review your personal chat usage and cost breakdowns."
action={
<div className="flex items-center gap-2 text-xs text-content-secondary">
<BarChart3Icon className="h-4 w-4" />
<span>{rangeLabel}</span>
</div>
}
/>
<ChatCostSummaryView
summary={summary}
isLoading={isLoading}
error={error}
onRetry={onRetry}
loadingLabel="Loading analytics"
emptyMessage="No usage data for you in this period."
/>
</div>
</div>
);
};
@@ -0,0 +1,95 @@
import { chatModelConfigs, chatModels, createChat } from "api/queries/chats";
import type * as TypesGen from "api/typesGenerated";
import type { FC } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { useNavigate } from "react-router";
import {
AgentCreateForm,
type CreateChatOptions,
} from "./components/AgentCreateForm";
import { AgentPageHeader } from "./components/AgentPageHeader";
import { ChimeButton } from "./components/ChimeButton";
import { WebPushButton } from "./components/WebPushButton";
import {
buildModelConfigIDByModelID,
getModelOptionsFromCatalog,
} from "./utils/modelOptions";
const lastModelConfigIDStorageKey = "agents.last-model-config-id";
const nilUUID = "00000000-0000-0000-0000-000000000000";
const AgentCreatePage: FC = () => {
const queryClient = useQueryClient();
const navigate = useNavigate();
const chatModelsQuery = useQuery(chatModels());
const chatModelConfigsQuery = useQuery(chatModelConfigs());
const createMutation = useMutation(createChat(queryClient));
const catalogModelOptions = getModelOptionsFromCatalog(
chatModelsQuery.data,
chatModelConfigsQuery.data,
);
const modelConfigIDByModelID = buildModelConfigIDByModelID(
chatModelConfigsQuery.data,
);
const handleCreateChat = async ({
message,
fileIDs,
workspaceId,
model,
}: CreateChatOptions) => {
const modelConfigID =
(model && modelConfigIDByModelID.get(model)) || nilUUID;
const content: TypesGen.ChatInputPart[] = [];
if (message.trim()) {
content.push({ type: "text", text: message });
}
if (fileIDs) {
for (const fileID of fileIDs) {
content.push({ type: "file", file_id: fileID });
}
}
const createdChat = await createMutation.mutateAsync({
content,
workspace_id: workspaceId,
model_config_id: modelConfigID,
});
if (typeof window !== "undefined") {
if (modelConfigID !== nilUUID) {
localStorage.setItem(lastModelConfigIDStorageKey, modelConfigID);
} else {
localStorage.removeItem(lastModelConfigIDStorageKey);
}
}
navigate(`/agents/${createdChat.id}`);
};
const handleOpenAnalytics = () => navigate("/agents/analytics");
return (
<>
<AgentPageHeader>
<ChimeButton />
<WebPushButton />
</AgentPageHeader>
<AgentCreateForm
onCreateChat={handleCreateChat}
isCreating={createMutation.isPending}
createError={createMutation.error}
modelCatalog={chatModelsQuery.data}
modelOptions={catalogModelOptions}
modelConfigs={chatModelConfigsQuery.data ?? []}
isModelCatalogLoading={chatModelsQuery.isLoading}
isModelConfigsLoading={chatModelConfigsQuery.isLoading}
modelCatalogError={chatModelsQuery.error}
onOpenAnalytics={handleOpenAnalytics}
/>
</>
);
};
export default AgentCreatePage;
@@ -51,49 +51,7 @@ 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,
onExpandSidebar: () => {},
} satisfies AgentsOutletContext
}
/>
@@ -1,11 +1,11 @@
import { act, renderHook } from "@testing-library/react";
import { createRef } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ChatMessageInputRef } from "./AgentChatInput";
import {
draftInputStorageKeyPrefix,
useConversationEditingState,
} from "./AgentDetail";
import type { ChatMessageInputRef } from "./components/AgentChatInput";
describe("useConversationEditingState", () => {
const chatID = "chat-abc-123";
+49 -27
View File
@@ -3,7 +3,10 @@ import { API, watchWorkspace } from "api/api";
import { isApiError } from "api/errors";
import {
chat,
chatDesktopEnabled,
chatMessagesForInfiniteScroll,
chatModelConfigs,
chatModels,
createChatMessage,
deleteChatQueuedMessage,
editChatMessage,
@@ -33,24 +36,33 @@ import type { UrlTransform } from "streamdown";
import { isMobileViewport } from "utils/mobile";
import { pageTitle } from "utils/page";
import { portForwardURL } from "utils/portForward";
import type { ChatMessageInputRef } from "./AgentChatInput";
import { useChatStore } from "./AgentDetail/ChatContext";
import { getParentChatID, getWorkspaceAgent } from "./AgentDetail/chatHelpers";
import { useWorkspaceCreationWatcher } from "./AgentDetail/useWorkspaceCreationWatcher";
import type { AgentsOutletContext } from "./AgentsPage";
import type { ChatMessageInputRef } from "./components/AgentChatInput";
import { useChatStore } from "./components/AgentDetail/ChatContext";
import {
getParentChatID,
getWorkspaceAgent,
} from "./components/AgentDetail/chatHelpers";
import { useWorkspaceCreationWatcher } from "./components/AgentDetail/useWorkspaceCreationWatcher";
import {
AgentDetailLoadingView,
AgentDetailNotFoundView,
AgentDetailView,
} from "./AgentDetailView";
import type { AgentsOutletContext } from "./AgentsPage";
} from "./components/AgentDetailView";
import { useGitWatcher } from "./hooks/useGitWatcher";
import {
buildModelConfigIDByModelID,
buildModelIDByConfigID,
getModelCatalogStatusMessage,
getModelOptionsFromCatalog,
getModelSelectorPlaceholder,
hasConfiguredModelsInCatalog,
} from "./modelOptions";
import { parsePullRequestUrl } from "./pullRequest";
import { formatUsageLimitMessage, isUsageLimitData } from "./usageLimitMessage";
import { useGitWatcher } from "./useGitWatcher";
} from "./utils/modelOptions";
import { parsePullRequestUrl } from "./utils/pullRequest";
import {
formatUsageLimitMessage,
isUsageLimitData,
} from "./utils/usageLimitMessage";
/** localStorage key controlling whether the right panel is visible. */
export const RIGHT_PANEL_OPEN_KEY = "agents.right-panel-open";
@@ -222,12 +234,6 @@ export function useConversationEditingState(deps: {
const AgentDetail: FC = () => {
const navigate = useNavigate();
const { agentId } = useParams<{ agentId: string }>();
const outletContext = useOutletContext<AgentsOutletContext>();
const queryClient = useQueryClient();
const [selectedModel, setSelectedModel] = useState("");
const [pendingEditMessageId, setPendingEditMessageId] = useState<
number | null
>(null);
const {
chatErrorReasons,
setChatErrorReason,
@@ -235,18 +241,14 @@ const AgentDetail: FC = () => {
requestArchiveAgent,
requestArchiveAndDeleteWorkspace,
requestUnarchiveAgent,
onOpenAnalytics,
isSidebarCollapsed,
onToggleSidebarCollapsed,
modelOptions,
modelConfigIDByModelID,
modelIDByConfigID,
modelConfigs,
modelCatalog,
isModelCatalogLoading,
modelCatalogError,
desktopEnabled,
} = outletContext;
} = useOutletContext<AgentsOutletContext>();
const queryClient = useQueryClient();
const [selectedModel, setSelectedModel] = useState("");
const [pendingEditMessageId, setPendingEditMessageId] = useState<
number | null
>(null);
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
const chatInputRef = useRef<ChatMessageInputRef | null>(null);
const inputValueRef = useRef(
@@ -293,6 +295,26 @@ const AgentDetail: FC = () => {
enabled: Boolean(workspaceId),
});
const chatModelsQuery = useQuery(chatModels());
const chatModelConfigsQuery = useQuery(chatModelConfigs());
const desktopEnabledQuery = useQuery(chatDesktopEnabled());
const desktopEnabled = desktopEnabledQuery.data?.enable_desktop ?? false;
const modelOptions = getModelOptionsFromCatalog(
chatModelsQuery.data,
chatModelConfigsQuery.data,
);
const modelConfigIDByModelID = buildModelConfigIDByModelID(
chatModelConfigsQuery.data,
);
const modelIDByConfigID = buildModelIDByConfigID(modelConfigIDByModelID);
const modelConfigs = chatModelConfigsQuery.data ?? [];
const modelCatalog = chatModelsQuery.data;
const isModelCatalogLoading = chatModelsQuery.isLoading;
const modelCatalogError = chatModelsQuery.error;
const handleOpenAnalytics = () => navigate("/agents/analytics");
// Subscribe to live workspace updates so that agent status changes
// (e.g. connected/disconnected) are reflected without a page refresh.
useEffect(() => {
@@ -839,7 +861,7 @@ const AgentDetail: FC = () => {
isInterruptPending={interruptMutation.isPending}
isSidebarCollapsed={isSidebarCollapsed}
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
onOpenAnalytics={onOpenAnalytics}
onOpenAnalytics={handleOpenAnalytics}
showSidebarPanel={showSidebarPanel}
onSetShowSidebarPanel={handleSetShowSidebarPanel}
prNumber={prNumber}
+6 -11
View File
@@ -9,8 +9,11 @@ import { type FC, useEffect, useRef, useState } from "react";
import { useMutation, useQueryClient } from "react-query";
import { Outlet, useParams } from "react-router";
import type { AgentsOutletContext } from "./AgentsPage";
import { bootstrapChatEmbedSession, EmbedProvider } from "./EmbedContext";
import type { ChatDetailError } from "./usageLimitMessage";
import {
bootstrapChatEmbedSession,
EmbedProvider,
} from "./components/EmbedContext";
import type { ChatDetailError } from "./utils/usageLimitMessage";
type BootstrapMessage = {
type: "coder:vscode-auth-bootstrap";
@@ -121,16 +124,8 @@ const AgentEmbedPage: FC = () => {
requestArchiveAndDeleteWorkspace,
isSidebarCollapsed,
onToggleSidebarCollapsed,
modelOptions: [],
modelConfigIDByModelID: new Map(),
modelIDByConfigID: new Map(),
modelConfigs: [],
modelCatalog: undefined,
isModelCatalogLoading: false,
modelCatalogError: null,
desktopEnabled: false,
onExpandSidebar: () => {},
};
// When signed out and not already bootstrapping, listen for the
// postMessage from the parent frame carrying the session token.
const isAwaitingBootstrapMessage =
@@ -0,0 +1,23 @@
import { useAuthenticated } from "hooks";
import type { FC } from "react";
import { useParams } from "react-router";
import { AgentSettingsPageView } from "./AgentSettingsPageView";
import { AgentPageHeader } from "./components/AgentPageHeader";
const AgentSettingsPage: FC = () => {
const { section } = useParams();
const { permissions } = useAuthenticated();
const isAgentsAdmin = permissions.editDeploymentConfig;
return (
<>
<AgentPageHeader />
<AgentSettingsPageView
activeSection={section ?? "behavior"}
canManageChatModelConfigs={isAgentsAdmin}
canSetSystemPrompt={isAgentsAdmin}
/>
</>
);
};
export default AgentSettingsPage;
@@ -6,7 +6,7 @@ import { userByNameKey } from "api/queries/users";
import type * as TypesGen from "api/typesGenerated";
import dayjs from "dayjs";
import { expect, spyOn, userEvent, waitFor, within } from "storybook/test";
import { SettingsPageContent } from "./SettingsPageContent";
import { AgentSettingsPageView } from "./AgentSettingsPageView";
// ── Usage mock helpers ─────────────────────────────────────────
@@ -128,8 +128,8 @@ const fixedNow = dayjs("2026-03-12T00:00:00Z");
// ── Meta ───────────────────────────────────────────────────────
const meta = {
title: "pages/AgentsPage/SettingsPageContent",
component: SettingsPageContent,
title: "pages/AgentsPage/AgentSettingsPageView",
component: AgentSettingsPageView,
decorators: [withAuthProvider, withDashboardProvider],
args: {
activeSection: "behavior",
@@ -161,10 +161,10 @@ const meta = {
});
spyOn(API, "updateChatWorkspaceTTL").mockResolvedValue();
},
} satisfies Meta<typeof SettingsPageContent>;
} satisfies Meta<typeof AgentSettingsPageView>;
export default meta;
type Story = StoryObj<typeof SettingsPageContent>;
type Story = StoryObj<typeof AgentSettingsPageView>;
// ── Behavior tab stories ───────────────────────────────────────
@@ -56,12 +56,12 @@ import {
DateRange,
type DateRangeValue,
} from "../TemplatePage/TemplateInsightsPage/DateRange";
import { ChatCostSummaryView } from "./ChatCostSummaryView";
import { ChatModelAdminPanel } from "./ChatModelAdminPanel/ChatModelAdminPanel";
import { InsightsContent } from "./InsightsContent";
import { LimitsTab } from "./LimitsTab";
import { MCPServerAdminPanel } from "./MCPServerAdminPanel";
import { SectionHeader } from "./SectionHeader";
import { ChatCostSummaryView } from "./components/ChatCostSummaryView";
import { ChatModelAdminPanel } from "./components/ChatModelAdminPanel/ChatModelAdminPanel";
import { InsightsContent } from "./components/InsightsContent";
import { LimitsTab } from "./components/LimitsTab";
import { MCPServerAdminPanel } from "./components/MCPServerAdminPanel";
import { SectionHeader } from "./components/SectionHeader";
const AdminBadge: FC = () => (
<TooltipProvider delayDuration={0}>
@@ -487,7 +487,7 @@ const UsageContent: FC<UsageContentProps> = ({ now }) => {
const textareaClassName =
"max-h-[240px] w-full resize-none overflow-y-auto rounded-lg border border-border bg-surface-primary px-4 py-3 font-sans text-[13px] leading-relaxed text-content-primary placeholder:text-content-secondary focus:outline-none focus:ring-2 focus:ring-content-link/30 [scrollbar-width:thin]";
interface SettingsPageContentProps {
interface AgentSettingsPageViewProps {
activeSection: string;
canManageChatModelConfigs: boolean;
canSetSystemPrompt: boolean;
@@ -496,7 +496,7 @@ interface SettingsPageContentProps {
now?: dayjs.Dayjs;
}
export const SettingsPageContent: FC<SettingsPageContentProps> = ({
export const AgentSettingsPageView: FC<AgentSettingsPageViewProps> = ({
activeSection,
canManageChatModelConfigs,
canSetSystemPrompt,
+4 -1
View File
@@ -1,6 +1,9 @@
import { act, renderHook } from "@testing-library/react";
import { beforeEach, describe, expect, it } from "vitest";
import { emptyInputStorageKey, useEmptyStateDraft } from "./AgentCreateForm";
import {
emptyInputStorageKey,
useEmptyStateDraft,
} from "./components/AgentCreateForm";
describe("useEmptyStateDraft", () => {
beforeEach(() => {
+15 -82
View File
@@ -2,12 +2,10 @@ import { API, watchChats } from "api/api";
import { getErrorMessage } from "api/errors";
import {
archiveChat,
chatDesktopEnabled,
chatDiffContentsKey,
chatKey,
chatModelConfigs,
chatModels,
createChat,
infiniteChats,
invalidateChatListQueries,
prependToInfiniteChatsCache,
@@ -30,24 +28,14 @@ import {
import { useNavigate, useParams } from "react-router";
import { toast } from "sonner";
import { createReconnectingWebSocket } from "utils/reconnectingWebSocket";
import {
type CreateChatOptions,
emptyInputStorageKey,
} from "./AgentCreateForm";
import { maybePlayChime } from "./AgentDetail/useAgentChime";
import { AgentsPageView } from "./AgentsPageView";
import { resolveArchiveAndDeleteAction } from "./agentWorkspaceUtils";
import {
getModelOptionsFromCatalog,
getNormalizedModelRef,
} from "./modelOptions";
import type { ChatDetailError } from "./usageLimitMessage";
import { useAgentsPageKeybindings } from "./useAgentsPageKeybindings";
import { useAgentsPWA } from "./useAgentsPWA";
const lastModelConfigIDStorageKey = "agents.last-model-config-id";
const nilUUID = "00000000-0000-0000-0000-000000000000";
const EMPTY_MODEL_CONFIGS: TypesGen.ChatModelConfig[] = [];
import { emptyInputStorageKey } from "./components/AgentCreateForm";
import { maybePlayChime } from "./components/AgentDetail/useAgentChime";
import { useAgentsPageKeybindings } from "./hooks/useAgentsPageKeybindings";
import { useAgentsPWA } from "./hooks/useAgentsPWA";
import { resolveArchiveAndDeleteAction } from "./utils/agentWorkspaceUtils";
import { getModelOptionsFromCatalog } from "./utils/modelOptions";
import type { ChatDetailError } from "./utils/usageLimitMessage";
// Type guard for SSE events from the chat list watch endpoint.
// Shallow-compare two ChatDiffStatus objects by their meaningful
@@ -93,11 +81,9 @@ const AgentsPage: FC = () => {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { agentId } = useParams();
const { permissions, user } = useAuthenticated();
const { permissions } = useAuthenticated();
const { appearance } = useDashboard();
const isAgentsAdmin =
permissions.editDeploymentConfig ||
user.roles.some((role) => role.name === "owner" || role.name === "admin");
const isAgentsAdmin = permissions.editDeploymentConfig;
const [archivedFilter, setArchivedFilter] = useState<"active" | "archived">(
"active",
@@ -149,10 +135,12 @@ const AgentsPage: FC = () => {
const chatsQuery = useInfiniteQuery(
infiniteChats({ archived: archivedFilter === "archived" }),
);
// Model queries are kept here for the sidebar, which displays
// model info alongside each chat. Child routes that need models
// subscribe to the same queries independently — react-query
// deduplicates the requests.
const chatModelsQuery = useQuery(chatModels());
const chatModelConfigsQuery = useQuery(chatModelConfigs());
const desktopEnabledQuery = useQuery(chatDesktopEnabled());
const createMutation = useMutation(createChat(queryClient));
const archiveChatBase = archiveChat(queryClient);
const archiveAgentMutation = useMutation({
...archiveChatBase,
@@ -208,24 +196,6 @@ const AgentsPage: FC = () => {
chatModelsQuery.data,
chatModelConfigsQuery.data,
);
const modelConfigIDByModelID = (() => {
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;
})();
const setChatErrorReason = (chatId: string, reason: ChatDetailError) => {
const trimmedMessage = reason.message.trim();
if (!chatId || !trimmedMessage) {
@@ -317,35 +287,6 @@ const AgentsPage: FC = () => {
};
const handleToggleSidebarCollapsed = () =>
setIsSidebarCollapsed((prev) => !prev);
const handleCreateChat = async (options: CreateChatOptions) => {
const { message, fileIDs, workspaceId, model } = options;
const modelConfigID =
(model && modelConfigIDByModelID.get(model)) || nilUUID;
const content: TypesGen.ChatInputPart[] = [];
if (message.trim()) {
content.push({ type: "text", text: message });
}
if (fileIDs) {
for (const fileID of fileIDs) {
content.push({ type: "file", file_id: fileID });
}
}
const createdChat = await createMutation.mutateAsync({
content,
workspace_id: workspaceId,
model_config_id: modelConfigID,
});
if (typeof window !== "undefined") {
if (modelConfigID !== nilUUID) {
localStorage.setItem(lastModelConfigIDStorageKey, modelConfigID);
} else {
localStorage.removeItem(lastModelConfigIDStorageKey);
}
}
navigate(`/agents/${createdChat.id}`);
};
const handleNewAgent = () => {
// Only clear the draft when the user is already on the empty
@@ -559,10 +500,10 @@ const AgentsPage: FC = () => {
agentId={agentId}
chatList={chatList}
catalogModelOptions={catalogModelOptions}
modelConfigs={chatModelConfigsQuery.data ?? EMPTY_MODEL_CONFIGS}
modelConfigs={chatModelConfigsQuery.data ?? []}
logoUrl={appearance.logo_url}
handleNewAgent={handleNewAgent}
isCreating={createMutation.isPending}
isCreating={false}
isArchiving={isArchiving}
archivingChatId={archivingChatId}
isChatsLoading={chatsQuery.isLoading}
@@ -578,14 +519,6 @@ const AgentsPage: FC = () => {
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()}
@@ -1,4 +1,8 @@
import { MockUserOwner } from "testHelpers/entities";
import {
MockNoPermissions,
MockPermissions,
MockUserOwner,
} from "testHelpers/entities";
import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { API } from "api/api";
@@ -18,6 +22,9 @@ import {
within,
} from "storybook/test";
import { reactRouterParameters } from "storybook-addon-remix-react-router";
import AgentAnalyticsPage from "./AgentAnalyticsPage";
import AgentCreatePage from "./AgentCreatePage";
import AgentSettingsPage from "./AgentSettingsPage";
import { AgentsPageView } from "./AgentsPageView";
const defaultModelOptions: ModelSelectorOption[] = [
@@ -103,9 +110,6 @@ const mockUsageUsers: TypesGen.ChatCostUsersResponse = {
],
};
// Use local noon so the rendered range label stays stable across timezones.
const fixedAnalyticsNow = dayjs("2026-03-12T12:00:00");
const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
const todayTimestamp = new Date().toISOString();
@@ -123,16 +127,21 @@ const buildChat = (overrides: Partial<Chat> = {}): Chat => ({
...overrides,
});
const agentsRouting = [
{ path: "/agents/settings/:section", useStoryElement: true },
{ path: "/agents/settings", useStoryElement: true },
{ path: "/agents/analytics", useStoryElement: true },
{ path: "/agents/:agentId", useStoryElement: true },
{ path: "/agents", useStoryElement: true },
] satisfies [
{ path: string; useStoryElement: boolean },
...{ path: string; useStoryElement: boolean }[],
];
// Use local noon so the rendered range label stays stable
// across timezones.
const fixedNow = dayjs("2026-03-12T12:00:00");
const agentsRouting = {
path: "/agents",
useStoryElement: true,
children: [
{ path: "settings", element: <AgentSettingsPage /> },
{ path: "settings/:section", element: <AgentSettingsPage /> },
{ path: "analytics", element: <AgentAnalyticsPage now={fixedNow} /> },
{ path: ":agentId", element: <div /> },
{ index: true, element: <AgentCreatePage /> },
],
};
const meta: Meta<typeof AgentsPageView> = {
title: "pages/AgentsPage/AgentsPageView",
@@ -141,6 +150,7 @@ const meta: Meta<typeof AgentsPageView> = {
parameters: {
layout: "fullscreen",
user: MockUserOwner,
permissions: MockPermissions,
reactRouter: reactRouterParameters({
location: { path: "/agents" },
routing: agentsRouting,
@@ -170,35 +180,11 @@ const meta: Meta<typeof AgentsPageView> = {
requestArchiveAndDeleteWorkspace: fn(),
onToggleSidebarCollapsed: fn(),
isAgentsAdmin: false,
analyticsNow: fixedAnalyticsNow,
archivedFilter: "active" as const,
onArchivedFilterChange: fn(),
hasNextPage: false,
onLoadMore: fn(),
isFetchingNextPage: false,
onCreateChat: fn(),
createError: 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({
@@ -217,6 +203,44 @@ const meta: Meta<typeof AgentsPageView> = {
spyOn(API, "updateUserChatCustomPrompt").mockResolvedValue({
custom_prompt: "",
});
// Mocks for child route pages that fetch their own data.
spyOn(API, "getChatModels").mockResolvedValue({
providers: [
{
provider: "openai",
available: true,
models: [
{
id: "openai:gpt-4o",
provider: "openai",
model: "gpt-4o",
display_name: "GPT-4o",
},
],
},
],
});
spyOn(API, "getChatModelConfigs").mockResolvedValue([
{
id: "config-openai-gpt-4o",
provider: "openai",
model: "gpt-4o",
display_name: "GPT-4o",
enabled: true,
is_default: false,
context_limit: 200000,
compression_threshold: 70,
created_at: "2026-02-18T00:00:00.000Z",
updated_at: "2026-02-18T00:00:00.000Z",
},
]);
spyOn(API, "getChatDesktopEnabled").mockResolvedValue({
enable_desktop: false,
});
spyOn(API, "getChatWorkspaceTTL").mockResolvedValue({
workspace_ttl_ms: 0,
});
spyOn(API, "updateChatWorkspaceTTL").mockResolvedValue();
},
};
@@ -489,6 +513,9 @@ export const OpensAnalyticsForNonAdmins: Story = {
args: {
isAgentsAdmin: false,
},
parameters: {
permissions: MockNoPermissions,
},
play: async ({ canvasElement }) => {
await openAnalyticsView(canvasElement);
@@ -523,6 +550,9 @@ export const OpensSettingsForNonAdmins: Story = {
args: {
isAgentsAdmin: false,
},
parameters: {
permissions: MockNoPermissions,
},
play: async ({ canvasElement }) => {
await openSettingsView(canvasElement);
+10 -121
View File
@@ -1,23 +1,14 @@
import type * as TypesGen from "api/typesGenerated";
import type { ModelSelectorOption } from "components/ai-elements";
import { Button } from "components/Button/Button";
import { ExternalImage } from "components/ExternalImage/ExternalImage";
import { CoderIcon } from "components/Icons/CoderIcon";
import type { Dayjs } from "dayjs";
import { PanelLeftIcon } from "lucide-react";
import type { FC } from "react";
import { NavLink, Outlet, useLocation, useNavigate } from "react-router";
import { Outlet, useLocation } from "react-router";
import { cn } from "utils/cn";
import { pageTitle } from "utils/page";
import { AgentCreateForm, type CreateChatOptions } from "./AgentCreateForm";
import { AgentsSidebar, sidebarViewFromPath } from "./AgentsSidebar";
import { AnalyticsPageContent } from "./AnalyticsPageContent";
import { ChimeButton } from "./ChimeButton";
import { SettingsPageContent } from "./SettingsPageContent";
import type { ChatDetailError } from "./usageLimitMessage";
import { WebPushButton } from "./WebPushButton";
type ChatModelOption = ModelSelectorOption;
import {
AgentsSidebar,
sidebarViewFromPath,
} from "./components/Sidebar/AgentsSidebar";
import type { ChatDetailError } from "./utils/usageLimitMessage";
export interface AgentsOutletContext {
chatErrorReasons: Record<string, ChatDetailError>;
@@ -31,21 +22,13 @@ export interface AgentsOutletContext {
) => 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;
onExpandSidebar: () => void;
}
interface AgentsPageViewProps {
agentId: string | undefined;
chatList: TypesGen.Chat[];
catalogModelOptions: readonly ChatModelOption[];
catalogModelOptions: readonly ModelSelectorOption[];
modelConfigs: readonly TypesGen.ChatModelConfig[];
logoUrl: string;
handleNewAgent: () => void;
@@ -69,20 +52,11 @@ interface AgentsPageViewProps {
) => void;
onToggleSidebarCollapsed: () => void;
isAgentsAdmin: boolean;
onCreateChat: (options: CreateChatOptions) => Promise<void>;
createError: unknown;
modelCatalog: TypesGen.ChatModelsResponse | null | undefined;
isModelCatalogLoading: boolean;
isModelConfigsLoading: boolean;
modelCatalogError: unknown;
modelConfigIDByModelID: ReadonlyMap<string, string>;
desktopEnabled: boolean;
hasNextPage: boolean | undefined;
onLoadMore: () => void;
isFetchingNextPage: boolean;
archivedFilter: "active" | "archived";
onArchivedFilterChange: (filter: "active" | "archived") => void;
analyticsNow?: Dayjs;
}
export const AgentsPageView: FC<AgentsPageViewProps> = ({
@@ -109,29 +83,15 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
requestArchiveAndDeleteWorkspace,
onToggleSidebarCollapsed,
isAgentsAdmin,
onCreateChat,
createError,
modelCatalog,
isModelCatalogLoading,
isModelConfigsLoading,
modelCatalogError,
modelConfigIDByModelID,
desktopEnabled,
hasNextPage,
onLoadMore,
isFetchingNextPage,
archivedFilter,
onArchivedFilterChange,
analyticsNow,
}) => {
const location = useLocation();
const navigate = useNavigate();
const sidebarView = sidebarViewFromPath(location.pathname);
const handleOpenAnalytics = () => {
navigate("/agents/analytics");
};
// The sidebar expects plain string error messages, but the outlet
// context now carries structured ChatDetailError objects.
const sidebarChatErrorReasons = Object.fromEntries(
@@ -141,16 +101,6 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
]),
);
const modelIDByConfigID = (() => {
const byConfigID = new Map<string, string>();
for (const [modelID, configID] of modelConfigIDByModelID.entries()) {
if (!byConfigID.has(configID)) {
byConfigID.set(configID, modelID);
}
}
return byConfigID;
})();
const outletContextValue: AgentsOutletContext = {
chatErrorReasons,
setChatErrorReason,
@@ -160,15 +110,7 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
requestArchiveAndDeleteWorkspace,
isSidebarCollapsed,
onToggleSidebarCollapsed,
onOpenAnalytics: handleOpenAnalytics,
modelOptions: catalogModelOptions,
modelConfigIDByModelID,
modelIDByConfigID,
modelConfigs,
modelCatalog,
isModelCatalogLoading,
modelCatalogError,
desktopEnabled,
onExpandSidebar,
};
return (
@@ -217,60 +159,7 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
"order-1 md:order-none flex-none md:flex-1",
)}
>
{sidebarView.panel === "settings" ? (
<SettingsPageContent
activeSection={sidebarView.section}
canManageChatModelConfigs={isAgentsAdmin}
canSetSystemPrompt={isAgentsAdmin}
/>
) : sidebarView.panel === "analytics" ? (
<AnalyticsPageContent now={analyticsNow} />
) : agentId ? (
<Outlet key={agentId} context={outletContextValue} />
) : (
<>
<div className="flex shrink-0 items-center gap-2 px-4 py-0.5">
<NavLink
to="/workspaces"
className="inline-flex shrink-0 md:hidden"
>
{logoUrl ? (
<ExternalImage className="h-6" src={logoUrl} alt="Logo" />
) : (
<CoderIcon className="h-6 w-6 fill-content-primary" />
)}
</NavLink>
{isSidebarCollapsed && (
<Button
variant="subtle"
size="icon"
onClick={onExpandSidebar}
aria-label="Expand sidebar"
className="hidden h-7 w-7 min-w-0 shrink-0 md:inline-flex"
>
<PanelLeftIcon />
</Button>
)}
<div className="flex min-w-0 flex-1 items-center" />
<div className="flex items-center gap-2">
<ChimeButton />
<WebPushButton />
</div>
</div>
<AgentCreateForm
onCreateChat={onCreateChat}
isCreating={isCreating}
createError={createError}
modelCatalog={modelCatalog}
modelOptions={catalogModelOptions}
modelConfigs={modelConfigs}
isModelCatalogLoading={isModelCatalogLoading}
isModelConfigsLoading={isModelConfigsLoading}
modelCatalogError={modelCatalogError}
onOpenAnalytics={handleOpenAnalytics}
/>
</>
)}
<Outlet context={outletContextValue} />
</div>
</div>
);
@@ -1,66 +0,0 @@
import { chatCostSummary } from "api/queries/chats";
import { useAuthContext } from "contexts/auth/AuthProvider";
import dayjs from "dayjs";
import { BarChart3Icon } from "lucide-react";
import type { FC } from "react";
import { useQuery } from "react-query";
import { ChatCostSummaryView } from "./ChatCostSummaryView";
import { SectionHeader } from "./SectionHeader";
const createDateRange = (now?: dayjs.Dayjs) => {
const end = now ?? dayjs();
const start = end.subtract(30, "day");
return {
startDate: start.toISOString(),
endDate: end.toISOString(),
rangeLabel: `${start.format("MMM D")} ${end.format("MMM D, YYYY")}`,
};
};
interface AnalyticsPageContentProps {
/** Override the current time for date range calculation.
* Used for deterministic Storybook snapshots.
*/
now?: dayjs.Dayjs;
}
export const AnalyticsPageContent: FC<AnalyticsPageContentProps> = ({
now,
}) => {
const { user } = useAuthContext();
const dateRange = createDateRange(now);
const summaryQuery = useQuery({
...chatCostSummary(user?.id ?? "me", {
start_date: dateRange.startDate,
end_date: dateRange.endDate,
}),
enabled: Boolean(user?.id),
});
return (
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto p-4 pt-8 [scrollbar-width:thin] [scrollbar-color:hsl(var(--surface-quaternary))_transparent]">
<div className="mx-auto w-full max-w-3xl">
<SectionHeader
label="Analytics"
description="Review your personal chat usage and cost breakdowns."
action={
<div className="flex items-center gap-2 text-xs text-content-secondary">
<BarChart3Icon className="h-4 w-4" />
<span>{dateRange.rangeLabel}</span>
</div>
}
/>
<ChatCostSummaryView
summary={summaryQuery.data}
isLoading={summaryQuery.isLoading}
error={summaryQuery.error}
onRetry={() => void summaryQuery.refetch()}
loadingLabel="Loading analytics"
emptyMessage="No usage data for you in this period."
/>
</div>
</div>
);
};
@@ -36,8 +36,8 @@ import {
} from "react";
import { cn } from "utils/cn";
import { isMobileViewport } from "utils/mobile";
import { formatProviderLabel } from "../utils/modelOptions";
import { ImageLightbox } from "./ImageLightbox";
import { formatProviderLabel } from "./modelOptions";
import { QueuedMessagesList } from "./QueuedMessagesList";
export type { ChatMessageInputRef } from "components/ChatMessageInput/ChatMessageInput";
@@ -24,15 +24,18 @@ import { useDashboard } from "modules/dashboard/useDashboard";
import { type FC, useEffect, useRef, useState } from "react";
import { useQuery } from "react-query";
import { toast } from "sonner";
import { AgentChatInput } from "./AgentChatInput";
import { useFileAttachments } from "../hooks/useFileAttachments";
import {
getModelCatalogStatusMessage,
getModelSelectorPlaceholder,
getNormalizedModelRef,
hasConfiguredModelsInCatalog,
} from "./modelOptions";
import { formatUsageLimitMessage, isUsageLimitData } from "./usageLimitMessage";
import { useFileAttachments } from "./useFileAttachments";
} from "../utils/modelOptions";
import {
formatUsageLimitMessage,
isUsageLimitData,
} from "../utils/usageLimitMessage";
import { AgentChatInput } from "./AgentChatInput";
/** @internal Exported for testing. */
export const emptyInputStorageKey = "agents.empty-input";
@@ -6,7 +6,7 @@ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
import { type InfiniteData, useQueryClient } from "react-query";
import type { OneWayMessageEvent } from "utils/OneWayWebSocket";
import { createReconnectingWebSocket } from "utils/reconnectingWebSocket";
import type { ChatDetailError } from "../usageLimitMessage";
import type { ChatDetailError } from "../../utils/usageLimitMessage";
import { applyMessagePartToStreamState } from "./streamState";
import type { StreamState } from "./types";
@@ -29,9 +29,9 @@ import {
} from "react";
import type { UrlTransform } from "streamdown";
import { cn } from "utils/cn";
import type { ChatDetailError } from "../../utils/usageLimitMessage";
import { ImageThumbnail } from "../AgentChatInput";
import { ImageLightbox } from "../ImageLightbox";
import type { ChatDetailError } from "../usageLimitMessage";
import { useSmoothStreamingText } from "./SmoothText";
import type {
MergedTool,
@@ -27,9 +27,9 @@ import type { FC } from "react";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import { cn } from "utils/cn";
import { parsePullRequestUrl } from "../../utils/pullRequest";
import { useEmbedContext } from "../EmbedContext";
import { PrStateIcon } from "../GitPanel";
import { parsePullRequestUrl } from "../pullRequest";
import { PrStateIcon } from "../GitPanel/GitPanel";
interface SidebarPanelState {
showSidebarPanel: boolean;
@@ -4,6 +4,8 @@ import { useDashboard } from "modules/dashboard/useDashboard";
import { type FC, useEffect } from "react";
import { toast } from "sonner";
import type { UrlTransform } from "streamdown";
import { useFileAttachments } from "../hooks/useFileAttachments";
import type { ChatDetailError } from "../utils/usageLimitMessage";
import {
AgentChatInput,
type ChatMessageInputRef,
@@ -30,8 +32,6 @@ import {
} from "./AgentDetail/messageParsing";
import { buildStreamTools } from "./AgentDetail/streamState";
import type { ParsedMessageEntry } from "./AgentDetail/types";
import type { ChatDetailError } from "./usageLimitMessage";
import { useFileAttachments } from "./useFileAttachments";
type ChatStoreHandle = ReturnType<typeof useChatStore>["store"];
@@ -7,6 +7,7 @@ import { type FC, type RefObject, useEffect, useRef, useState } from "react";
import type { UrlTransform } from "streamdown";
import { cn } from "utils/cn";
import { pageTitle } from "utils/page";
import type { ChatDetailError } from "../utils/usageLimitMessage";
import { AgentChatInput, type ChatMessageInputRef } from "./AgentChatInput";
import {
selectChatStatus,
@@ -19,10 +20,9 @@ import {
ChatConversationSkeleton,
RightPanelSkeleton,
} from "./AgentsSkeletons";
import { GitPanel } from "./GitPanel";
import { RightPanel } from "./RightPanel";
import { SidebarTabView } from "./SidebarTabView";
import type { ChatDetailError } from "./usageLimitMessage";
import { GitPanel } from "./GitPanel/GitPanel";
import { RightPanel } from "./RightPanel/RightPanel";
import { SidebarTabView } from "./Sidebar/SidebarTabView";
type ChatStoreHandle = ReturnType<typeof useChatStore>["store"];
@@ -0,0 +1,44 @@
import { Button } from "components/Button/Button";
import { ExternalImage } from "components/ExternalImage/ExternalImage";
import { CoderIcon } from "components/Icons/CoderIcon";
import { PanelLeftIcon } from "lucide-react";
import { useDashboard } from "modules/dashboard/useDashboard";
import type { FC, ReactNode } from "react";
import { NavLink, useOutletContext } from "react-router";
import type { AgentsOutletContext } from "../AgentsPageView";
interface AgentPageHeaderProps {
children?: ReactNode;
}
export const AgentPageHeader: FC<AgentPageHeaderProps> = ({ children }) => {
const { isSidebarCollapsed, onExpandSidebar } =
useOutletContext<AgentsOutletContext>();
const { appearance } = useDashboard();
const logoUrl = appearance.logo_url;
return (
<div className="flex shrink-0 items-center gap-2 px-4 py-0.5">
<NavLink to="/workspaces" className="inline-flex shrink-0 md:hidden">
{logoUrl ? (
<ExternalImage className="h-6" src={logoUrl} alt="Logo" />
) : (
<CoderIcon className="h-6 w-6 fill-content-primary" />
)}
</NavLink>
{isSidebarCollapsed && (
<Button
variant="subtle"
size="icon"
onClick={onExpandSidebar}
aria-label="Expand sidebar"
className="hidden h-7 w-7 min-w-0 shrink-0 md:inline-flex"
>
<PanelLeftIcon />
</Button>
)}
<div className="min-w-0 flex-1" />
{children && <div className="flex items-center gap-2">{children}</div>}
</div>
);
};
@@ -16,7 +16,7 @@ import { Spinner } from "components/Spinner/Spinner";
import { type FC, type ReactNode, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { cn } from "utils/cn";
import { formatProviderLabel } from "../modelOptions";
import { formatProviderLabel } from "../../utils/modelOptions";
import { normalizeProvider, readOptionalString } from "./helpers";
import { ModelsSection } from "./ModelsSection";
import { ProvidersSection } from "./ProvidersSection";
@@ -10,7 +10,7 @@ import {
} from "components/Tooltip/Tooltip";
import { ChevronLeftIcon, InfoIcon } from "lucide-react";
import { type FC, type FormEvent, useId, useState } from "react";
import { formatProviderLabel } from "../modelOptions";
import { formatProviderLabel } from "../../utils/modelOptions";
import type { ProviderState } from "./ChatModelAdminPanel";
import { readOptionalString } from "./helpers";
import { ProviderIcon } from "./ProviderIcon";
@@ -2,7 +2,7 @@ import { ExternalImage } from "components/ExternalImage/ExternalImage";
import { ServerIcon } from "lucide-react";
import type { FC } from "react";
import { cn } from "utils/cn";
import { formatProviderLabel } from "../modelOptions";
import { formatProviderLabel } from "../../utils/modelOptions";
import { normalizeProvider } from "./helpers";
const providerIconMap: Record<string, string> = {
@@ -12,9 +12,6 @@ import {
useRef,
useState,
} from "react";
import type { ChatMessageInputRef } from "./AgentChatInput";
import type { DiffStyle } from "./DiffViewer";
import { DiffViewer } from "./DiffViewer";
import {
annotationLineForBox,
annotationSideForBox,
@@ -22,7 +19,10 @@ import {
commentBoxFromRange,
contentRangeForBox,
selectedLinesForBox,
} from "./diffCommentSelection";
} from "../../utils/diffCommentSelection";
import type { ChatMessageInputRef } from "../AgentChatInput";
import type { DiffStyle } from "../DiffViewer/DiffViewer";
import { DiffViewer } from "../DiffViewer/DiffViewer";
// -------------------------------------------------------------------
// Diff content extraction
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { DiffStatBadge } from "./DiffStats";
import { DiffStatBadge } from "../DiffViewer/DiffStats";
const badgeMeta: Meta<typeof DiffStatBadge> = {
title: "pages/AgentsPage/DiffStatBadge",
@@ -2,9 +2,9 @@ import type { DiffLineAnnotation, SelectedLineRange } from "@pierre/diffs";
import { parsePatchFiles } from "@pierre/diffs";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, waitFor } from "storybook/test";
import type { DiffStyle } from "./DiffViewer";
import { DiffViewer } from "./DiffViewer";
import { InlinePromptInput } from "./RemoteDiffPanel";
import type { DiffStyle } from "../DiffViewer/DiffViewer";
import { DiffViewer } from "../DiffViewer/DiffViewer";
import { InlinePromptInput } from "../DiffViewer/RemoteDiffPanel";
// biome-ignore format: raw diff string must preserve exact whitespace
const sampleDiff = [
@@ -24,7 +24,7 @@ import {
useState,
} from "react";
import { cn } from "utils/cn";
import { changeColor, changeLabel } from "./diffColors";
import { changeColor, changeLabel } from "../../utils/diffColors";
// -------------------------------------------------------------------
// Public interface
@@ -1,9 +1,9 @@
import { parsePatchFiles } from "@pierre/diffs";
import type { WorkspaceAgentRepoChanges } from "api/typesGenerated";
import type { FC, RefObject } from "react";
import type { ChatMessageInputRef } from "./AgentChatInput";
import { CommentableDiffViewer } from "./CommentableDiffViewer";
import type { DiffStyle } from "./DiffViewer";
import type { ChatMessageInputRef } from "../AgentChatInput";
import { CommentableDiffViewer } from "../DiffViewer/CommentableDiffViewer";
import type { DiffStyle } from "../DiffViewer/DiffViewer";
interface LocalDiffPanelProps {
repo: WorkspaceAgentRepoChanges;
@@ -14,13 +14,13 @@ import {
import { type FC, type RefObject, useEffect, useState } from "react";
import { useQuery } from "react-query";
import { cn } from "utils/cn";
import type { ChatMessageInputRef } from "./AgentChatInput";
import { CommentableDiffViewer } from "./CommentableDiffViewer";
import { DiffStatBadge } from "./DiffStats";
import type { DiffStyle } from "./DiffViewer";
import { parsePullRequestUrl } from "./pullRequest";
import { parsePullRequestUrl } from "../../utils/pullRequest";
import type { ChatMessageInputRef } from "../AgentChatInput";
import { CommentableDiffViewer } from "../DiffViewer/CommentableDiffViewer";
import { DiffStatBadge } from "../DiffViewer/DiffStats";
import type { DiffStyle } from "../DiffViewer/DiffViewer";
export { InlinePromptInput } from "./CommentableDiffViewer";
export { InlinePromptInput } from "../DiffViewer/CommentableDiffViewer";
// -------------------------------------------------------------------
// PR state badge
@@ -1,6 +1,6 @@
import { parsePatchFiles } from "@pierre/diffs";
import { describe, expect, it } from "vitest";
import { extractDiffContent } from "./CommentableDiffViewer";
import { extractDiffContent } from "../DiffViewer/CommentableDiffViewer";
function parse(diffStr: string) {
return parsePatchFiles(diffStr).flatMap((p) => p.files);
@@ -19,11 +19,15 @@ import {
} from "lucide-react";
import { type FC, type RefObject, useEffect, useRef, useState } from "react";
import { cn } from "utils/cn";
import type { ChatMessageInputRef } from "./AgentChatInput";
import { DiffStatBadge } from "./DiffStats";
import { type DiffStyle, loadDiffStyle, saveDiffStyle } from "./DiffViewer";
import { LocalDiffPanel } from "./LocalDiffPanel";
import { RemoteDiffPanel } from "./RemoteDiffPanel";
import type { ChatMessageInputRef } from "../AgentChatInput";
import { DiffStatBadge } from "../DiffViewer/DiffStats";
import {
type DiffStyle,
loadDiffStyle,
saveDiffStyle,
} from "../DiffViewer/DiffViewer";
import { LocalDiffPanel } from "../DiffViewer/LocalDiffPanel";
import { RemoteDiffPanel } from "../DiffViewer/RemoteDiffPanel";
type GitView = { type: "remote" } | { type: "local"; repoRoot: string };
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, userEvent, within } from "storybook/test";
import { InlinePromptInput } from "./RemoteDiffPanel";
import { InlinePromptInput } from "../DiffViewer/RemoteDiffPanel";
const meta: Meta<typeof InlinePromptInput> = {
title: "pages/AgentsPage/InlinePromptInput",
@@ -21,7 +21,7 @@ import type { FC } from "react";
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
import { cn } from "utils/cn";
import { formatCostMicros } from "utils/currency";
import { PrStateIcon } from "./GitPanel";
import { PrStateIcon } from "./GitPanel/GitPanel";
dayjs.extend(relativeTime);

Some files were not shown because too many files have changed in this diff Show More