diff --git a/site/src/pages/AgentsPage/AgentsPage.stories.tsx b/site/src/pages/AgentsPage/AgentCreateForm.stories.tsx similarity index 95% rename from site/src/pages/AgentsPage/AgentsPage.stories.tsx rename to site/src/pages/AgentsPage/AgentCreateForm.stories.tsx index fd8254e9a4..91815f846d 100644 --- a/site/src/pages/AgentsPage/AgentsPage.stories.tsx +++ b/site/src/pages/AgentsPage/AgentCreateForm.stories.tsx @@ -11,7 +11,7 @@ import { waitFor, within, } from "storybook/test"; -import { AgentsEmptyState } from "./AgentsPage"; +import { AgentCreateForm } from "./AgentsPage"; const modelOptions = [ { @@ -22,9 +22,9 @@ const modelOptions = [ }, ] as const; -const meta: Meta = { - title: "pages/AgentsPage/AgentsEmptyState", - component: AgentsEmptyState, +const meta: Meta = { + title: "pages/AgentsPage/AgentCreateForm", + component: AgentCreateForm, decorators: [withDashboardProvider], args: { onCreateChat: fn(), @@ -55,7 +55,7 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = {}; diff --git a/site/src/pages/AgentsPage/AgentDetail.tsx b/site/src/pages/AgentsPage/AgentDetail.tsx index 54ae970c6b..8b6fb32cef 100644 --- a/site/src/pages/AgentsPage/AgentDetail.tsx +++ b/site/src/pages/AgentsPage/AgentDetail.tsx @@ -15,8 +15,6 @@ import { deploymentSSHConfig } from "api/queries/deployment"; import { workspaceById, workspaceByIdKey } from "api/queries/workspaces"; import type * as TypesGen from "api/typesGenerated"; import type { ModelSelectorOption } from "components/ai-elements"; -import { Skeleton } from "components/Skeleton/Skeleton"; -import { ArchiveIcon } from "lucide-react"; import { getTerminalHref, getVSCodeHref, @@ -34,7 +32,6 @@ import { import { useMutation, useQuery, useQueryClient } from "react-query"; import { useNavigate, useOutletContext, useParams } from "react-router"; import { toast } from "sonner"; -import { cn } from "utils/cn"; import { pageTitle } from "utils/page"; import { AgentChatInput, @@ -66,32 +63,23 @@ import { parseMessagesWithMergedTools, } from "./AgentDetail/messageParsing"; import { buildStreamTools } from "./AgentDetail/streamState"; -import { AgentDetailTopBar } from "./AgentDetail/TopBar"; import { useMessageWindow } from "./AgentDetail/useMessageWindow"; import { useWorkspaceCreationWatcher } from "./AgentDetail/useWorkspaceCreationWatcher"; +import { + AgentDetailLoadingView, + AgentDetailNotFoundView, + AgentDetailView, +} from "./AgentDetailView"; import type { AgentsOutletContext } from "./AgentsPage"; -import { GitPanel } from "./GitPanel"; import { getModelCatalogStatusMessage, getModelOptionsFromCatalog, getModelSelectorPlaceholder, hasConfiguredModelsInCatalog, } from "./modelOptions"; -import { RightPanel } from "./RightPanel"; -import { type SidebarTab, SidebarTabView } from "./SidebarTabView"; import { useFileAttachments } from "./useFileAttachments"; import { useGitWatcher } from "./useGitWatcher"; -const noopSetChatErrorReason: AgentsOutletContext["setChatErrorReason"] = - () => {}; -const noopClearChatErrorReason: AgentsOutletContext["clearChatErrorReason"] = - () => {}; -const noopRequestArchiveAgent: AgentsOutletContext["requestArchiveAgent"] = - () => {}; -const noopRequestArchiveAndDeleteWorkspace: AgentsOutletContext["requestArchiveAndDeleteWorkspace"] = - () => {}; -const noopRequestUnarchiveAgent: AgentsOutletContext["requestUnarchiveAgent"] = - () => {}; const lastModelConfigIDStorageKey = "agents.last-model-config-id"; /** @internal Exported for testing. */ export const draftInputStorageKeyPrefix = "agents.draft-input."; @@ -114,7 +102,7 @@ interface AgentDetailTimelineProps { savingMessageId?: number | null; } -const AgentDetailTimeline: FC = ({ +export const AgentDetailTimeline: FC = ({ store, chatID, persistedErrorReason, @@ -229,7 +217,7 @@ interface AgentDetailInputProps { }[]; } -const AgentDetailInput: FC = ({ +export const AgentDetailInput: FC = ({ store, compressionThreshold, onSend, @@ -560,36 +548,22 @@ export function useConversationEditingState(deps: { const AgentDetail: FC = () => { const navigate = useNavigate(); const { agentId } = useParams<{ agentId: string }>(); - const outletContext = useOutletContext(); + const outletContext = useOutletContext(); const queryClient = useQueryClient(); const [selectedModel, setSelectedModel] = useState(""); - const [showSidebarPanel, setShowSidebarPanel] = useState(false); - const [isRightPanelExpanded, setIsRightPanelExpanded] = useState(false); - // Tracks the live visual expanded state during drag so sibling - // content hides/shows in real-time rather than on pointer-up. - // Null means "no drag override, use isRightPanelExpanded". - const [dragVisualExpanded, setDragVisualExpanded] = useState( - null, - ); - const visualExpanded = dragVisualExpanded ?? isRightPanelExpanded; const [pendingEditMessageId, setPendingEditMessageId] = useState< number | null >(null); - const chatErrorReasons = outletContext?.chatErrorReasons ?? {}; - const setChatErrorReason = - outletContext?.setChatErrorReason ?? noopSetChatErrorReason; - const clearChatErrorReason = - outletContext?.clearChatErrorReason ?? noopClearChatErrorReason; - const requestArchiveAgent = - outletContext?.requestArchiveAgent ?? noopRequestArchiveAgent; - const requestArchiveAndDeleteWorkspace = - outletContext?.requestArchiveAndDeleteWorkspace ?? - noopRequestArchiveAndDeleteWorkspace; - const requestUnarchiveAgent = - outletContext?.requestUnarchiveAgent ?? noopRequestUnarchiveAgent; - const isSidebarCollapsed = outletContext?.isSidebarCollapsed ?? false; - const onToggleSidebarCollapsed = - outletContext?.onToggleSidebarCollapsed ?? (() => {}); + const { + chatErrorReasons, + setChatErrorReason, + clearChatErrorReason, + requestArchiveAgent, + requestArchiveAndDeleteWorkspace, + requestUnarchiveAgent, + isSidebarCollapsed, + onToggleSidebarCollapsed, + } = outletContext; const scrollContainerRef = useRef(null); const chatInputRef = useRef(null); const inputValueRef = useRef(""); @@ -632,7 +606,6 @@ const AgentDetail: FC = () => { const chatModelsQuery = useQuery(chatModels()); const chatModelConfigsQuery = useQuery(chatModelConfigs()); const sshConfigQuery = useQuery(deploymentSSHConfig()); - const hasDiffStatus = Boolean(diffStatusQuery.data?.url); const workspace = workspaceQuery.data; const workspaceAgent = getWorkspaceAgent(workspace, undefined); const chatData = chatQuery.data; @@ -642,16 +615,6 @@ const AgentDetail: FC = () => { const chatQueuedMessages = chatData?.queued_messages; const chatLastModelConfigID = chatRecord?.last_model_config_id; - // Auto-open the diff panel when diff status first appears. - // See: https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes - const [prevHasDiffStatus, setPrevHasDiffStatus] = useState(false); - if (hasDiffStatus !== prevHasDiffStatus) { - setPrevHasDiffStatus(hasDiffStatus); - if (hasDiffStatus && !window.matchMedia("(max-width: 767px)").matches) { - setShowSidebarPanel(true); - } - } - const modelOptions = useMemo( () => getModelOptionsFromCatalog( @@ -739,17 +702,6 @@ const AgentDetail: FC = () => { chatInputRef.current?.focus(); }, []); - // Auto-open sidebar when git watcher receives its first non-empty - // repositories update. - const [prevHasGitRepos, setPrevHasGitRepos] = useState(false); - const hasGitRepos = gitWatcher.repositories.size > 0; - if (hasGitRepos !== prevHasGitRepos) { - setPrevHasGitRepos(hasGitRepos); - if (hasGitRepos && !window.matchMedia("(max-width: 767px)").matches) { - setShowSidebarPanel(true); - } - } - // Extract PR number from diff status URL. const prMatch = diffStatusQuery.data?.url?.match(/\/pull\/(\d+)/)?.[1]; const prNumber = prMatch ? Number(prMatch) : undefined; @@ -996,7 +948,6 @@ const AgentDetail: FC = () => { workspace && workspaceAgent && sshConfigQuery.data?.hostname_suffix ? `ssh ${workspaceAgent.name}.${workspace.name}.${workspace.owner_name}.${sshConfigQuery.data.hostname_suffix}` : undefined; - const shouldShowSidebar = showSidebarPanel; const generateKeyMutation = useMutation({ mutationFn: () => API.getApiKey(), @@ -1064,255 +1015,77 @@ const AgentDetail: FC = () => { if (chatQuery.isLoading) { return ( -
- {titleElement} - {}, - }} - workspace={{ - canOpenEditors: false, - canOpenWorkspace: false, - onOpenInEditor: () => {}, - onViewWorkspace: () => {}, - onOpenTerminal: () => {}, - sshCommand: undefined, - }} - onOpenParentChat={() => {}} - onArchiveAgent={() => {}} - onUnarchiveAgent={() => {}} - onArchiveAndDeleteWorkspace={() => {}} - hasWorkspace={false} - isSidebarCollapsed={isSidebarCollapsed} - onToggleSidebarCollapsed={onToggleSidebarCollapsed} - /> -
-
-
-
- {/* User message bubble (right-aligned) */} -
- -
- {/* Assistant response lines (left-aligned) */} -
- - - -
- {/* Second user message bubble */} -
- -
- {/* Second assistant response */} -
- - - - - -
{" "} -
-
-
-
-
- {}} - initialValue="" - isDisabled={isInputDisabled} - isLoading={false} - selectedModel={effectiveSelectedModel} - onModelChange={setSelectedModel} - modelOptions={modelOptions} - modelSelectorPlaceholder={modelSelectorPlaceholder} - hasModelOptions={hasModelOptions} - inputStatusText={inputStatusText} - modelCatalogStatusMessage={modelCatalogStatusMessage} - /> -
-
+ ); } if (!chatQuery.data || !agentId) { return ( -
- {titleElement} - {}, - }} - workspace={{ - canOpenEditors: false, - canOpenWorkspace: false, - onOpenInEditor: () => {}, - onViewWorkspace: () => {}, - onOpenTerminal: () => {}, - sshCommand: undefined, - }} - onOpenParentChat={() => {}} - onArchiveAgent={() => {}} - onUnarchiveAgent={() => {}} - onArchiveAndDeleteWorkspace={() => {}} - hasWorkspace={false} - isSidebarCollapsed={isSidebarCollapsed} - onToggleSidebarCollapsed={onToggleSidebarCollapsed} - /> -
- Chat not found -
{" "} -
+ ); } return ( -
- {titleElement} -
-
- navigate(`/agents/${chatId}`)} - panel={{ - showSidebarPanel, - onToggleSidebar: () => setShowSidebarPanel((prev) => !prev), - }} - workspace={{ - canOpenEditors, - canOpenWorkspace, - onOpenInEditor: handleOpenInEditor, - onViewWorkspace: handleViewWorkspace, - onOpenTerminal: handleOpenTerminal, - sshCommand, - }} - onArchiveAgent={handleArchiveAgentAction} - onUnarchiveAgent={handleUnarchiveAgentAction} - onArchiveAndDeleteWorkspace={handleArchiveAndDeleteWorkspaceAction} - hasWorkspace={Boolean(workspaceId)} - isArchived={isArchived} - isSidebarCollapsed={isSidebarCollapsed} - onToggleSidebarCollapsed={onToggleSidebarCollapsed} - /> - {isArchived && ( -
- - This agent has been archived and is read-only. -
- )} -
-
-
-
- -
-
-
- -
-
- setIsRightPanelExpanded((prev) => !prev)} - onClose={() => setShowSidebarPanel(false)} - onVisualExpandedChange={setDragVisualExpanded} - isSidebarCollapsed={isSidebarCollapsed} - onToggleSidebarCollapsed={onToggleSidebarCollapsed} - > - - ), - }, - ].filter(Boolean) as SidebarTab[] - } - onClose={() => setShowSidebarPanel(false)} - isExpanded={visualExpanded} - onToggleExpanded={() => setIsRightPanelExpanded((prev) => !prev)} - isSidebarCollapsed={isSidebarCollapsed} - onToggleSidebarCollapsed={onToggleSidebarCollapsed} - chatTitle={chatTitle} - /> - {" "} -
+ navigate(`/agents/${chatId}`)} + handleInterrupt={handleInterrupt} + handleDeleteQueuedMessage={handleDeleteQueuedMessage} + handlePromoteQueuedMessage={handlePromoteQueuedMessage} + handleArchiveAgentAction={handleArchiveAgentAction} + handleUnarchiveAgentAction={handleUnarchiveAgentAction} + handleArchiveAndDeleteWorkspaceAction={ + handleArchiveAndDeleteWorkspaceAction + } + scrollContainerRef={scrollContainerRef} + /> ); }; diff --git a/site/src/pages/AgentsPage/AgentDetailView.stories.tsx b/site/src/pages/AgentsPage/AgentDetailView.stories.tsx new file mode 100644 index 0000000000..81ecc6d8ad --- /dev/null +++ b/site/src/pages/AgentsPage/AgentDetailView.stories.tsx @@ -0,0 +1,318 @@ +import { MockUserOwner } from "testHelpers/entities"; +import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { ChatDiffStatusResponse } from "api/api"; +import type * as TypesGen from "api/typesGenerated"; +import type { ModelSelectorOption } from "components/ai-elements"; +import { fn } from "storybook/test"; +import { reactRouterParameters } from "storybook-addon-remix-react-router"; +import { createChatStore } from "./AgentDetail/ChatContext"; +import { + AgentDetailLoadingView, + AgentDetailNotFoundView, + AgentDetailView, +} from "./AgentDetailView"; + +// --------------------------------------------------------------------------- +// Shared constants & helpers +// --------------------------------------------------------------------------- +const AGENT_ID = "agent-detail-view-1"; + +const defaultModelOptions: ModelSelectorOption[] = [ + { + id: "openai:gpt-4o", + provider: "openai", + model: "gpt-4o", + displayName: "GPT-4o", + }, +]; + +const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); + +const buildChat = (overrides: Partial = {}): TypesGen.Chat => ({ + id: AGENT_ID, + owner_id: "owner-1", + title: "Help me refactor", + status: "completed", + last_model_config_id: "model-config-1", + created_at: oneWeekAgo, + updated_at: oneWeekAgo, + archived: false, + last_error: null, + ...overrides, +}); + +const defaultEditing = { + chatInputRef: { current: null }, + editorInitialValue: "", + editingMessageId: null, + editingFileBlocks: [] as readonly { + mediaType: string; + data?: string; + fileId?: string; + }[], + handleEditUserMessage: fn(), + handleCancelHistoryEdit: fn(), + editingQueuedMessageID: null, + handleStartQueueEdit: fn(), + handleCancelQueueEdit: fn(), + handleSendFromInput: fn(), + handleContentChange: fn(), +}; + +const defaultGitWatcher: { + repositories: ReadonlyMap; + refresh: () => void; +} = { + repositories: new Map(), + refresh: fn(), +}; + +const agentsRouting = [ + { path: "/agents/:agentId", useStoryElement: true }, + { path: "/agents", useStoryElement: true }, +] satisfies [ + { path: string; useStoryElement: boolean }, + ...{ path: string; useStoryElement: boolean }[], +]; + +// --------------------------------------------------------------------------- +// Meta +// --------------------------------------------------------------------------- +const meta: Meta = { + title: "pages/AgentsPage/AgentDetailView", + component: AgentDetailView, + decorators: [withAuthProvider, withDashboardProvider], + parameters: { + layout: "fullscreen", + user: MockUserOwner, + reactRouter: reactRouterParameters({ + location: { + path: `/agents/${AGENT_ID}`, + pathParams: { agentId: AGENT_ID }, + }, + routing: agentsRouting, + }), + }, + args: { + agentId: AGENT_ID, + chatTitle: "Help me refactor", + parentChat: undefined, + chatErrorReasons: {}, + chatRecord: buildChat(), + isArchived: false, + hasWorkspace: true, + store: createChatStore(), + editing: defaultEditing, + pendingEditMessageId: null, + effectiveSelectedModel: "openai:gpt-4o", + setSelectedModel: fn(), + modelOptions: defaultModelOptions, + modelSelectorPlaceholder: "Select a model", + hasModelOptions: true, + inputStatusText: null, + modelCatalogStatusMessage: null, + compressionThreshold: undefined, + isInputDisabled: false, + isSubmissionPending: false, + isInterruptPending: false, + isSidebarCollapsed: false, + onToggleSidebarCollapsed: fn(), + prNumber: undefined, + diffStatusData: undefined, + gitWatcher: defaultGitWatcher, + canOpenEditors: false, + canOpenWorkspace: false, + sshCommand: undefined, + handleOpenInEditor: fn(), + handleViewWorkspace: fn(), + handleOpenTerminal: fn(), + handleCommit: fn(), + onNavigateToChat: fn(), + handleInterrupt: fn(), + handleDeleteQueuedMessage: fn(), + handlePromoteQueuedMessage: fn(), + handleArchiveAgentAction: fn(), + handleUnarchiveAgentAction: fn(), + handleArchiveAndDeleteWorkspaceAction: fn(), + scrollContainerRef: { current: null }, + }, +}; + +export default meta; +type Story = StoryObj; + +// --------------------------------------------------------------------------- +// AgentDetailView stories +// --------------------------------------------------------------------------- + +/** Basic conversation view with a chat title, workspace, and no archive. */ +export const Default: Story = {}; + +/** Archived agent displays the read-only banner below the top bar. */ +export const Archived: Story = { + args: { + isArchived: true, + chatRecord: buildChat({ archived: true }), + isInputDisabled: true, + }, +}; + +/** Shows the parent chat link in the top bar when a parent exists. */ +export const WithParentChat: Story = { + args: { + parentChat: buildChat({ + id: "parent-chat-1", + title: "Root agent", + }), + }, +}; + +/** Persisted error reason shown in the timeline area. */ +export const WithError: Story = { + args: { + chatErrorReasons: { [AGENT_ID]: "Model rate limited" }, + }, +}; + +/** Input area appears disabled when `isInputDisabled` is true. */ +export const InputDisabled: Story = { + args: { + isInputDisabled: true, + }, +}; + +/** Shows a sending/pending state for the input. */ +export const SubmissionPending: Story = { + args: { + isSubmissionPending: true, + }, +}; + +/** Right sidebar panel is open with diff status data. */ +export const WithSidebarPanel: Story = { + args: { + prNumber: 123, + diffStatusData: { + chat_id: AGENT_ID, + url: "https://github.com/coder/coder/pull/123", + changes_requested: false, + additions: 42, + deletions: 7, + changed_files: 5, + } satisfies ChatDiffStatusResponse, + }, +}; + +/** Left sidebar is collapsed. */ +export const SidebarCollapsed: Story = { + args: { + isSidebarCollapsed: true, + }, +}; + +/** No model options available — shows a disabled status message. */ +export const NoModelOptions: Story = { + args: { + hasModelOptions: false, + modelOptions: [], + inputStatusText: "No models configured. Ask an admin.", + isInputDisabled: true, + }, +}; + +/** Top bar has workspace action buttons visible. */ +export const WithWorkspaceActions: Story = { + args: { + canOpenEditors: true, + canOpenWorkspace: true, + sshCommand: "ssh coder.workspace", + }, +}; + +// --------------------------------------------------------------------------- +// AgentDetailLoadingView stories +// --------------------------------------------------------------------------- + +/** Default loading state with skeleton placeholders. */ +export const Loading: Story = { + render: () => ( + Loading — Agents} + isInputDisabled + effectiveSelectedModel="openai:gpt-4o" + setSelectedModel={fn()} + modelOptions={defaultModelOptions} + modelSelectorPlaceholder="Select a model" + hasModelOptions + inputStatusText={null} + modelCatalogStatusMessage={null} + isSidebarCollapsed={false} + onToggleSidebarCollapsed={fn()} + /> + ), +}; + +/** Loading state with the model selector populated. */ +export const LoadingWithModelOptions: Story = { + render: () => ( + Loading — Agents} + isInputDisabled={false} + effectiveSelectedModel="openai:gpt-4o" + setSelectedModel={fn()} + modelOptions={defaultModelOptions} + modelSelectorPlaceholder="Select a model" + hasModelOptions + inputStatusText={null} + modelCatalogStatusMessage={null} + isSidebarCollapsed={false} + onToggleSidebarCollapsed={fn()} + /> + ), +}; + +/** Loading state with the left sidebar collapsed. */ +export const LoadingSidebarCollapsed: Story = { + render: () => ( + Loading — Agents} + isInputDisabled + effectiveSelectedModel="openai:gpt-4o" + setSelectedModel={fn()} + modelOptions={defaultModelOptions} + modelSelectorPlaceholder="Select a model" + hasModelOptions + inputStatusText={null} + modelCatalogStatusMessage={null} + isSidebarCollapsed + onToggleSidebarCollapsed={fn()} + /> + ), +}; + +// --------------------------------------------------------------------------- +// AgentDetailNotFoundView stories +// --------------------------------------------------------------------------- + +/** Shows the "Chat not found" message. */ +export const NotFound: Story = { + render: () => ( + Not Found — Agents} + isSidebarCollapsed={false} + onToggleSidebarCollapsed={fn()} + /> + ), +}; + +/** "Chat not found" with the left sidebar collapsed. */ +export const NotFoundSidebarCollapsed: Story = { + render: () => ( + Not Found — Agents} + isSidebarCollapsed + onToggleSidebarCollapsed={fn()} + /> + ), +}; diff --git a/site/src/pages/AgentsPage/AgentDetailView.tsx b/site/src/pages/AgentsPage/AgentDetailView.tsx new file mode 100644 index 0000000000..d4bc074a50 --- /dev/null +++ b/site/src/pages/AgentsPage/AgentDetailView.tsx @@ -0,0 +1,488 @@ +import type { ChatDiffStatusResponse } from "api/api"; +import type * as TypesGen from "api/typesGenerated"; +import type { ModelSelectorOption } from "components/ai-elements"; +import { Skeleton } from "components/Skeleton/Skeleton"; +import { ArchiveIcon } from "lucide-react"; +import { type FC, type RefObject, useState } from "react"; +import { cn } from "utils/cn"; +import { pageTitle } from "utils/page"; +import { AgentChatInput, type ChatMessageInputRef } from "./AgentChatInput"; +import { AgentDetailInput, AgentDetailTimeline } from "./AgentDetail"; +import type { useChatStore } from "./AgentDetail/ChatContext"; +import { AgentDetailTopBar } from "./AgentDetail/TopBar"; +import { GitPanel } from "./GitPanel"; +import { RightPanel } from "./RightPanel"; +import { type SidebarTab, SidebarTabView } from "./SidebarTabView"; + +type ChatStoreHandle = ReturnType["store"]; + +// Re-use the inner presentational components directly. They are + +interface EditingState { + chatInputRef: RefObject; + editorInitialValue: string; + editingMessageId: number | null; + editingFileBlocks: readonly { + mediaType: string; + data?: string; + fileId?: string; + }[]; + handleEditUserMessage: ( + messageId: number, + text: string, + fileBlocks?: readonly { + mediaType: string; + data?: string; + fileId?: string; + }[], + ) => void; + handleCancelHistoryEdit: () => void; + editingQueuedMessageID: number | null; + handleStartQueueEdit: (id: number, text: string) => void; + handleCancelQueueEdit: () => void; + handleSendFromInput: (message: string, fileIds?: string[]) => void; + handleContentChange: (content: string) => void; +} + +interface AgentDetailViewProps { + // Chat data. + agentId: string; + chatTitle: string | undefined; + parentChat: TypesGen.Chat | undefined; + chatErrorReasons: Record; + chatRecord: TypesGen.Chat | undefined; + isArchived: boolean; + hasWorkspace: boolean; + + // Store handle. + store: ChatStoreHandle; + + // Editing state. + editing: EditingState; + pendingEditMessageId: number | null; + + // Model/input configuration. + effectiveSelectedModel: string; + setSelectedModel: (model: string) => void; + modelOptions: readonly ModelSelectorOption[]; + modelSelectorPlaceholder: string; + hasModelOptions: boolean; + inputStatusText: string | null; + modelCatalogStatusMessage: string | null; + compressionThreshold: number | undefined; + isInputDisabled: boolean; + isSubmissionPending: boolean; + isInterruptPending: boolean; + + // Sidebar / panel state. + isSidebarCollapsed: boolean; + onToggleSidebarCollapsed: () => void; + + // Sidebar content data. + prNumber: number | undefined; + diffStatusData: ChatDiffStatusResponse | undefined; + gitWatcher: { + repositories: ReadonlyMap; + refresh: () => void; + }; + + // Workspace action handlers. + canOpenEditors: boolean; + canOpenWorkspace: boolean; + sshCommand: string | undefined; + handleOpenInEditor: (editor: "cursor" | "vscode") => void; + handleViewWorkspace: () => void; + handleOpenTerminal: () => void; + handleCommit: (repoRoot: string) => void; + + // Navigation. + onNavigateToChat: (chatId: string) => void; + + // Chat action handlers. + handleInterrupt: () => void; + handleDeleteQueuedMessage: (id: number) => Promise; + handlePromoteQueuedMessage: (id: number) => Promise; + + // Archive actions. + handleArchiveAgentAction: () => void; + handleUnarchiveAgentAction: () => void; + handleArchiveAndDeleteWorkspaceAction: () => void; + + // Scroll container ref. + scrollContainerRef: RefObject; +} + +export const AgentDetailView: FC = ({ + agentId, + chatTitle, + parentChat, + chatErrorReasons, + chatRecord, + isArchived, + hasWorkspace, + store, + editing, + pendingEditMessageId, + effectiveSelectedModel, + setSelectedModel, + modelOptions, + modelSelectorPlaceholder, + hasModelOptions, + inputStatusText, + modelCatalogStatusMessage, + compressionThreshold, + isInputDisabled, + isSubmissionPending, + isInterruptPending, + isSidebarCollapsed, + onToggleSidebarCollapsed, + prNumber, + diffStatusData, + gitWatcher, + canOpenEditors, + canOpenWorkspace, + sshCommand, + handleOpenInEditor, + handleViewWorkspace, + handleOpenTerminal, + handleCommit, + onNavigateToChat, + handleInterrupt, + handleDeleteQueuedMessage, + handlePromoteQueuedMessage, + handleArchiveAgentAction, + handleUnarchiveAgentAction, + handleArchiveAndDeleteWorkspaceAction, + scrollContainerRef, +}) => { + // Panel/sidebar UI state – purely visual, no data-fetching + // implications. + const [showSidebarPanel, setShowSidebarPanel] = useState(false); + const [isRightPanelExpanded, setIsRightPanelExpanded] = useState(false); + const [dragVisualExpanded, setDragVisualExpanded] = useState( + null, + ); + const visualExpanded = dragVisualExpanded ?? isRightPanelExpanded; + + // Derive trivial booleans the View can compute itself. + const hasDiffStatus = Boolean(diffStatusData?.url); + const hasGitRepos = gitWatcher.repositories.size > 0; + + // Auto-open the diff panel when diff status first appears. + // See: https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes + const [prevHasDiffStatus, setPrevHasDiffStatus] = useState(false); + if (hasDiffStatus !== prevHasDiffStatus) { + setPrevHasDiffStatus(hasDiffStatus); + if (hasDiffStatus && !window.matchMedia("(max-width: 767px)").matches) { + setShowSidebarPanel(true); + } + } + + // Auto-open sidebar when git watcher receives its first non-empty + // repositories update. + const [prevHasGitRepos, setPrevHasGitRepos] = useState(false); + if (hasGitRepos !== prevHasGitRepos) { + setPrevHasGitRepos(hasGitRepos); + if (hasGitRepos && !window.matchMedia("(max-width: 767px)").matches) { + setShowSidebarPanel(true); + } + } + + const titleElement = ( + + {chatTitle ? pageTitle(chatTitle, "Agents") : pageTitle("Agents")} + + ); + + const shouldShowSidebar = showSidebarPanel; + + return ( +
+ {titleElement} +
+
+ onNavigateToChat(chatId)} + panel={{ + showSidebarPanel, + onToggleSidebar: () => setShowSidebarPanel((prev) => !prev), + }} + workspace={{ + canOpenEditors, + canOpenWorkspace, + onOpenInEditor: handleOpenInEditor, + onViewWorkspace: handleViewWorkspace, + onOpenTerminal: handleOpenTerminal, + sshCommand, + }} + onArchiveAgent={handleArchiveAgentAction} + onUnarchiveAgent={handleUnarchiveAgentAction} + onArchiveAndDeleteWorkspace={handleArchiveAndDeleteWorkspaceAction} + hasWorkspace={hasWorkspace} + isArchived={isArchived} + isSidebarCollapsed={isSidebarCollapsed} + onToggleSidebarCollapsed={onToggleSidebarCollapsed} + /> + {isArchived && ( +
+ + This agent has been archived and is read-only. +
+ )} +
+
+
+
+ +
+
+
+ +
+
+ setIsRightPanelExpanded((prev) => !prev)} + onClose={() => setShowSidebarPanel(false)} + onVisualExpandedChange={setDragVisualExpanded} + isSidebarCollapsed={isSidebarCollapsed} + onToggleSidebarCollapsed={onToggleSidebarCollapsed} + > + + ), + }, + ].filter(Boolean) as SidebarTab[] + } + onClose={() => setShowSidebarPanel(false)} + isExpanded={visualExpanded} + onToggleExpanded={() => setIsRightPanelExpanded((prev) => !prev)} + isSidebarCollapsed={isSidebarCollapsed} + onToggleSidebarCollapsed={onToggleSidebarCollapsed} + chatTitle={chatTitle} + /> + {" "} +
+ ); +}; + +interface AgentDetailLoadingViewProps { + titleElement: React.ReactNode; + isInputDisabled: boolean; + effectiveSelectedModel: string; + setSelectedModel: (model: string) => void; + modelOptions: readonly ModelSelectorOption[]; + modelSelectorPlaceholder: string; + hasModelOptions: boolean; + inputStatusText: string | null; + modelCatalogStatusMessage: string | null; + isSidebarCollapsed: boolean; + onToggleSidebarCollapsed: () => void; +} + +export const AgentDetailLoadingView: FC = ({ + titleElement, + isInputDisabled, + effectiveSelectedModel, + setSelectedModel, + modelOptions, + modelSelectorPlaceholder, + hasModelOptions, + inputStatusText, + modelCatalogStatusMessage, + isSidebarCollapsed, + onToggleSidebarCollapsed, +}) => { + return ( +
+ {titleElement} + {}, + }} + workspace={{ + canOpenEditors: false, + canOpenWorkspace: false, + onOpenInEditor: () => {}, + onViewWorkspace: () => {}, + onOpenTerminal: () => {}, + sshCommand: undefined, + }} + onOpenParentChat={() => {}} + onArchiveAgent={() => {}} + onUnarchiveAgent={() => {}} + onArchiveAndDeleteWorkspace={() => {}} + hasWorkspace={false} + isSidebarCollapsed={isSidebarCollapsed} + onToggleSidebarCollapsed={onToggleSidebarCollapsed} + /> +
+
+
+
+ {/* User message bubble (right-aligned) */} +
+ +
+ {/* Assistant response lines (left-aligned) */} +
+ + + +
+ {/* Second user message bubble */} +
+ +
+ {/* Second assistant response */} +
+ + + + + +
{" "} +
+
+
+
+
+ {}} + initialValue="" + isDisabled={isInputDisabled} + isLoading={false} + selectedModel={effectiveSelectedModel} + onModelChange={setSelectedModel} + modelOptions={modelOptions} + modelSelectorPlaceholder={modelSelectorPlaceholder} + hasModelOptions={hasModelOptions} + inputStatusText={inputStatusText} + modelCatalogStatusMessage={modelCatalogStatusMessage} + /> +
+
+ ); +}; + +interface AgentDetailNotFoundViewProps { + titleElement: React.ReactNode; + isSidebarCollapsed: boolean; + onToggleSidebarCollapsed: () => void; +} + +export const AgentDetailNotFoundView: FC = ({ + titleElement, + isSidebarCollapsed, + onToggleSidebarCollapsed, +}) => { + return ( +
+ {titleElement} + {}, + }} + workspace={{ + canOpenEditors: false, + canOpenWorkspace: false, + onOpenInEditor: () => {}, + onViewWorkspace: () => {}, + onOpenTerminal: () => {}, + sshCommand: undefined, + }} + onOpenParentChat={() => {}} + onArchiveAgent={() => {}} + onUnarchiveAgent={() => {}} + onArchiveAndDeleteWorkspace={() => {}} + hasWorkspace={false} + isSidebarCollapsed={isSidebarCollapsed} + onToggleSidebarCollapsed={onToggleSidebarCollapsed} + /> +
+ Chat not found +
{" "} +
+ ); +}; diff --git a/site/src/pages/AgentsPage/AgentsPage.tsx b/site/src/pages/AgentsPage/AgentsPage.tsx index 0ead639647..28b5c1de3d 100644 --- a/site/src/pages/AgentsPage/AgentsPage.tsx +++ b/site/src/pages/AgentsPage/AgentsPage.tsx @@ -19,7 +19,6 @@ import type * as TypesGen from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { ChevronDownIcon } from "components/AnimatedIcons/ChevronDown"; import type { ModelSelectorOption } from "components/ai-elements"; -import { Button } from "components/Button/Button"; import { Combobox, ComboboxContent, @@ -29,10 +28,8 @@ import { ComboboxList, ComboboxTrigger, } from "components/Combobox/Combobox"; -import { ExternalImage } from "components/ExternalImage/ExternalImage"; -import { CoderIcon } from "components/Icons/CoderIcon"; import { useAuthenticated } from "hooks"; -import { MonitorIcon, PanelLeftIcon } from "lucide-react"; +import { MonitorIcon } from "lucide-react"; import { useDashboard } from "modules/dashboard/useDashboard"; import { type FC, @@ -44,15 +41,13 @@ import { useState, } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; -import { NavLink, Outlet, useNavigate, useParams } from "react-router"; +import { useNavigate, useParams } from "react-router"; import { toast } from "sonner"; -import { cn } from "utils/cn"; -import { pageTitle } from "utils/page"; import { createReconnectingWebSocket } from "utils/reconnectingWebSocket"; import { AgentChatInput } from "./AgentChatInput"; import { maybePlayChime } from "./AgentDetail/useAgentChime"; -import { AgentsSidebar } from "./AgentsSidebar"; -import { ChimeButton } from "./ChimeButton"; +import type { AgentsOutletContext } from "./AgentsPageView"; +import { AgentsPageView } from "./AgentsPageView"; import { ConfigureAgentsDialog } from "./ConfigureAgentsDialog"; import { getModelCatalogStatusMessage, @@ -63,7 +58,6 @@ import { import { useAgentsPageKeybindings } from "./useAgentsPageKeybindings"; import { useAgentsPWA } from "./useAgentsPWA"; import { useFileAttachments } from "./useFileAttachments"; -import { WebPushButton } from "./WebPushButton"; /** @internal Exported for testing. */ export const emptyInputStorageKey = "agents.empty-input"; @@ -73,7 +67,7 @@ const nilUUID = "00000000-0000-0000-0000-000000000000"; type ChatModelOption = ModelSelectorOption; -type CreateChatOptions = { +export type CreateChatOptions = { message: string; fileIDs?: string[]; workspaceId?: string; @@ -94,19 +88,7 @@ function isChatListSSEEvent( ); } -export interface AgentsOutletContext { - chatErrorReasons: Record; - setChatErrorReason: (chatId: string, reason: string) => void; - clearChatErrorReason: (chatId: string) => void; - requestArchiveAgent: (chatId: string) => void; - requestUnarchiveAgent: (chatId: string) => void; - requestArchiveAndDeleteWorkspace: ( - chatId: string, - workspaceId: string, - ) => void; - isSidebarCollapsed: boolean; - onToggleSidebarCollapsed: () => void; -} +export type { AgentsOutletContext } from "./AgentsPageView"; const AgentsPage: FC = () => { useAgentsPWA(); @@ -118,7 +100,6 @@ const AgentsPage: FC = () => { const isAgentsAdmin = permissions.editDeploymentConfig || user.roles.some((role) => role.name === "owner" || role.name === "admin"); - const canSetSystemPrompt = isAgentsAdmin; // The global CSS sets scrollbar-gutter: stable on to prevent // layout shift on pages that toggle scrollbars. The agents page @@ -207,8 +188,6 @@ const AgentsPage: FC = () => { toast.error(getErrorMessage(error, "Failed to unarchive agent.")); }, }); - const [isConfigureAgentsDialogOpen, setConfigureAgentsDialogOpen] = - useState(false); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); const [chatErrorReasons, setChatErrorReasons] = useState< Record @@ -514,108 +493,31 @@ const AgentsPage: FC = () => { }); return ( -
- {pageTitle("Agents")} -
- void chatsQuery.refetch()} - onCollapse={() => setIsSidebarCollapsed(true)} - /> -
- -
- {agentId ? ( - - ) : ( - <> -
- - {appearance.logo_url ? ( - - ) : ( - - )} - - {isSidebarCollapsed && ( - - )} -
-
- - {" "} - {isAgentsAdmin && ( - - )} -
-
- - - )} -
-
+ void chatsQuery.refetch()} + onCollapseSidebar={() => setIsSidebarCollapsed(true)} + isSidebarCollapsed={isSidebarCollapsed} + onExpandSidebar={() => setIsSidebarCollapsed(false)} + outletContext={outletContext} + onCreateChat={handleCreateChat} + createError={createMutation.error} + modelCatalog={chatModelsQuery.data} + isModelCatalogLoading={chatModelsQuery.isLoading} + isModelConfigsLoading={chatModelConfigsQuery.isLoading} + modelCatalogError={chatModelsQuery.error} + isAgentsAdmin={isAgentsAdmin} + /> ); }; @@ -673,7 +575,7 @@ export function useEmptyStateDraft() { }; } -interface AgentsEmptyStateProps { +interface AgentCreateFormProps { onCreateChat: (options: CreateChatOptions) => Promise; isCreating: boolean; createError: unknown; @@ -689,7 +591,7 @@ interface AgentsEmptyStateProps { onConfigureAgentsDialogOpenChange: (open: boolean) => void; } -export const AgentsEmptyState: FC = ({ +export const AgentCreateForm: FC = ({ onCreateChat, isCreating, createError, diff --git a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx new file mode 100644 index 0000000000..d4744af583 --- /dev/null +++ b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx @@ -0,0 +1,301 @@ +import { MockUserOwner } from "testHelpers/entities"; +import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { API } from "api/api"; +import type * as TypesGen from "api/typesGenerated"; +import type { Chat } from "api/typesGenerated"; +import type { ModelSelectorOption } from "components/ai-elements"; +import { fn, spyOn } from "storybook/test"; +import { reactRouterParameters } from "storybook-addon-remix-react-router"; +import { AgentsPageView } from "./AgentsPageView"; + +const defaultModelOptions: ModelSelectorOption[] = [ + { + id: "openai:gpt-4o", + provider: "openai", + model: "gpt-4o", + displayName: "GPT-4o", + }, +]; + +const defaultModelConfigs: TypesGen.ChatModelConfig[] = [ + { + 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", + }, +]; + +const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); +const todayTimestamp = new Date().toISOString(); + +const buildChat = (overrides: Partial = {}): Chat => ({ + id: "chat-default", + owner_id: "owner-1", + title: "Agent", + status: "completed", + last_model_config_id: defaultModelConfigs[0].id, + created_at: oneWeekAgo, + updated_at: oneWeekAgo, + archived: false, + last_error: null, + ...overrides, +}); + +const agentsRouting = [ + { path: "/agents/:agentId", useStoryElement: true }, + { path: "/agents", useStoryElement: true }, +] satisfies [ + { path: string; useStoryElement: boolean }, + ...{ path: string; useStoryElement: boolean }[], +]; + +const meta: Meta = { + title: "pages/AgentsPage/AgentsPageView", + component: AgentsPageView, + decorators: [withAuthProvider, withDashboardProvider], + parameters: { + layout: "fullscreen", + user: MockUserOwner, + reactRouter: reactRouterParameters({ + location: { path: "/agents" }, + routing: agentsRouting, + }), + }, + args: { + agentId: undefined, + chatList: [], + catalogModelOptions: defaultModelOptions, + modelConfigs: defaultModelConfigs, + logoUrl: "", + handleNewAgent: fn(), + isCreating: false, + isArchiving: false, + archivingChatId: undefined, + isChatsLoading: false, + chatsLoadError: null, + onRetryChatsLoad: fn(), + onCollapseSidebar: fn(), + isSidebarCollapsed: false, + onExpandSidebar: fn(), + outletContext: { + chatErrorReasons: {}, + setChatErrorReason: fn(), + clearChatErrorReason: fn(), + requestArchiveAgent: fn(), + requestUnarchiveAgent: fn(), + requestArchiveAndDeleteWorkspace: fn(), + isSidebarCollapsed: false, + onToggleSidebarCollapsed: fn(), + }, + isAgentsAdmin: false, + onCreateChat: fn(), + createError: undefined, + modelCatalog: undefined, + isModelCatalogLoading: false, + isModelConfigsLoading: false, + modelCatalogError: undefined, + }, + beforeEach: () => { + spyOn(API, "getWorkspaces").mockResolvedValue({ + workspaces: [], + count: 0, + }); + }, +}; + +export default meta; +type Story = StoryObj; + +export const EmptyState: Story = {}; + +export const WithChatList: Story = { + args: { + chatList: [ + buildChat({ + id: "chat-1", + title: "Refactor authentication module", + status: "completed", + updated_at: todayTimestamp, + }), + buildChat({ + id: "chat-2", + title: "Add unit tests for API layer", + status: "running", + updated_at: todayTimestamp, + }), + buildChat({ + id: "chat-3", + title: "Fix database migration issue", + status: "error", + last_error: "Connection timeout", + updated_at: todayTimestamp, + }), + buildChat({ + id: "chat-4", + title: "Update CI/CD pipeline config", + status: "waiting", + updated_at: todayTimestamp, + }), + buildChat({ + id: "chat-5", + title: "Implement WebSocket handler", + status: "completed", + updated_at: todayTimestamp, + }), + buildChat({ + id: "chat-6", + title: "Debug memory leak in worker", + status: "paused", + updated_at: todayTimestamp, + }), + ], + }, +}; + +export const LoadingChats: Story = { + args: { + isChatsLoading: true, + chatList: [], + }, +}; + +export const ChatsLoadError: Story = { + args: { + chatsLoadError: new Error("Failed to fetch chats"), + }, +}; + +export const SidebarCollapsed: Story = { + args: { + isSidebarCollapsed: true, + chatList: [ + buildChat({ + id: "chat-1", + title: "Collapsed sidebar agent", + updated_at: todayTimestamp, + }), + ], + outletContext: { + chatErrorReasons: {}, + setChatErrorReason: fn(), + clearChatErrorReason: fn(), + requestArchiveAgent: fn(), + requestUnarchiveAgent: fn(), + requestArchiveAndDeleteWorkspace: fn(), + isSidebarCollapsed: true, + onToggleSidebarCollapsed: fn(), + }, + }, +}; + +export const WithToolbarEndContent: Story = { + args: { + isAgentsAdmin: true, + }, +}; + +export const CreatingAgent: Story = { + args: { + isCreating: true, + chatList: [ + buildChat({ + id: "chat-1", + title: "Existing agent", + updated_at: todayTimestamp, + }), + ], + }, +}; + +export const ArchivingAgent: Story = { + args: { + isArchiving: true, + archivingChatId: "chat-1", + chatList: [ + buildChat({ + id: "chat-1", + title: "Agent being archived", + updated_at: todayTimestamp, + }), + buildChat({ + id: "chat-2", + title: "Another agent", + updated_at: todayTimestamp, + }), + ], + }, +}; + +export const WithAgentSelected: Story = { + args: { + agentId: "chat-1", + chatList: [ + buildChat({ + id: "chat-1", + title: "Selected agent", + status: "running", + updated_at: todayTimestamp, + }), + buildChat({ + id: "chat-2", + title: "Another agent", + updated_at: todayTimestamp, + }), + ], + }, + parameters: { + reactRouter: reactRouterParameters({ + location: { + path: "/agents/chat-1", + pathParams: { agentId: "chat-1" }, + }, + routing: agentsRouting, + }), + }, +}; + +export const WithErrorReasons: Story = { + args: { + chatList: [ + buildChat({ + id: "chat-1", + title: "Rate limited agent", + status: "error", + updated_at: todayTimestamp, + }), + buildChat({ + id: "chat-2", + title: "Healthy agent", + status: "running", + updated_at: todayTimestamp, + }), + buildChat({ + id: "chat-3", + title: "Another errored agent", + status: "error", + updated_at: todayTimestamp, + }), + ], + outletContext: { + chatErrorReasons: { + "chat-1": "Model rate limited", + "chat-3": "Context window exceeded", + }, + setChatErrorReason: fn(), + clearChatErrorReason: fn(), + requestArchiveAgent: fn(), + requestUnarchiveAgent: fn(), + requestArchiveAndDeleteWorkspace: fn(), + isSidebarCollapsed: false, + onToggleSidebarCollapsed: fn(), + }, + }, +}; diff --git a/site/src/pages/AgentsPage/AgentsPageView.tsx b/site/src/pages/AgentsPage/AgentsPageView.tsx new file mode 100644 index 0000000000..42de3f742a --- /dev/null +++ b/site/src/pages/AgentsPage/AgentsPageView.tsx @@ -0,0 +1,191 @@ +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 { PanelLeftIcon } from "lucide-react"; +import { type FC, useState } from "react"; +import { NavLink, Outlet } from "react-router"; +import { cn } from "utils/cn"; +import { pageTitle } from "utils/page"; +import { AgentCreateForm, type CreateChatOptions } from "./AgentsPage"; +import { AgentsSidebar } from "./AgentsSidebar"; +import { ChimeButton } from "./ChimeButton"; +import { WebPushButton } from "./WebPushButton"; + +type ChatModelOption = ModelSelectorOption; + +export interface AgentsOutletContext { + chatErrorReasons: Record; + setChatErrorReason: (chatId: string, reason: string) => void; + clearChatErrorReason: (chatId: string) => void; + requestArchiveAgent: (chatId: string) => void; + requestUnarchiveAgent: (chatId: string) => void; + requestArchiveAndDeleteWorkspace: ( + chatId: string, + workspaceId: string, + ) => void; + isSidebarCollapsed: boolean; + onToggleSidebarCollapsed: () => void; +} + +interface AgentsPageViewProps { + agentId: string | undefined; + chatList: TypesGen.Chat[]; + catalogModelOptions: readonly ChatModelOption[]; + modelConfigs: readonly TypesGen.ChatModelConfig[]; + logoUrl: string; + handleNewAgent: () => void; + isCreating: boolean; + isArchiving: boolean; + archivingChatId: string | undefined; + isChatsLoading: boolean; + chatsLoadError: Error | null; + onRetryChatsLoad: () => void; + onCollapseSidebar: () => void; + isSidebarCollapsed: boolean; + onExpandSidebar: () => void; + outletContext: AgentsOutletContext; + isAgentsAdmin: boolean; + onCreateChat: (options: CreateChatOptions) => Promise; + createError: unknown; + modelCatalog: TypesGen.ChatModelsResponse | null | undefined; + isModelCatalogLoading: boolean; + isModelConfigsLoading: boolean; + modelCatalogError: unknown; +} + +export const AgentsPageView: FC = ({ + agentId, + chatList, + catalogModelOptions, + modelConfigs, + logoUrl, + handleNewAgent, + isCreating, + isArchiving, + archivingChatId, + isChatsLoading, + chatsLoadError, + onRetryChatsLoad, + onCollapseSidebar, + isSidebarCollapsed, + onExpandSidebar, + outletContext, + isAgentsAdmin, + onCreateChat, + createError, + modelCatalog, + isModelCatalogLoading, + isModelConfigsLoading, + modelCatalogError, +}) => { + const { + chatErrorReasons, + requestArchiveAgent, + requestUnarchiveAgent, + requestArchiveAndDeleteWorkspace, + } = outletContext; + const [isConfigureAgentsDialogOpen, setConfigureAgentsDialogOpen] = + useState(false); + return ( +
+ {pageTitle("Agents")} +
+ +
+ +
+ {agentId ? ( + + ) : ( + <> +
+ + {logoUrl ? ( + + ) : ( + + )} + + {isSidebarCollapsed && ( + + )} +
+
+ + + {isAgentsAdmin && ( + + )} +
+
+ + + )} +
+
+ ); +};