From e99b41fa9aba6ad8c71115b869ff97124bceeecc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 02:57:04 +0000 Subject: [PATCH] feat: add frontend UI for agent file management Co-authored-by: abhi1693 <5083532+abhi1693@users.noreply.github.com> --- .../src/app/agents/[agentId]/files/page.tsx | 375 ++++++++++++++++++ frontend/src/app/agents/[agentId]/page.tsx | 8 + 2 files changed, 383 insertions(+) create mode 100644 frontend/src/app/agents/[agentId]/files/page.tsx diff --git a/frontend/src/app/agents/[agentId]/files/page.tsx b/frontend/src/app/agents/[agentId]/files/page.tsx new file mode 100644 index 00000000..9edb4e90 --- /dev/null +++ b/frontend/src/app/agents/[agentId]/files/page.tsx @@ -0,0 +1,375 @@ +"use client"; + +export const dynamic = "force-dynamic"; + +import { useEffect, useState } from "react"; +import { useParams, useRouter } from "next/navigation"; + +import { useAuth } from "@/auth/clerk"; + +import { ApiError } from "@/api/mutator"; +import { + type getAgentApiV1AgentsAgentIdGetResponse, + useGetAgentApiV1AgentsAgentIdGet, +} from "@/api/generated/agents/agents"; +import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useOrganizationMembership } from "@/lib/use-organization-membership"; +import type { AgentRead } from "@/api/generated/model"; + +export default function AgentFilesPage() { + const { isSignedIn } = useAuth(); + const router = useRouter(); + const params = useParams(); + const agentIdParam = params?.agentId; + const agentId = Array.isArray(agentIdParam) ? agentIdParam[0] : agentIdParam; + + const { isAdmin } = useOrganizationMembership(isSignedIn); + + const [files, setFiles] = useState>([]); + const [selectedFile, setSelectedFile] = useState(null); + const [fileContent, setFileContent] = useState(""); + const [editDialogOpen, setEditDialogOpen] = useState(false); + const [importDialogOpen, setImportDialogOpen] = useState(false); + const [importFileName, setImportFileName] = useState(""); + const [importFileContent, setImportFileContent] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const agentQuery = useGetAgentApiV1AgentsAgentIdGet< + getAgentApiV1AgentsAgentIdGetResponse, + ApiError + >(agentId ?? "", { + query: { + enabled: Boolean(isSignedIn && isAdmin && agentId), + refetchOnMount: "always", + retry: false, + }, + }); + + const agent: AgentRead | null = + agentQuery.data?.status === 200 ? agentQuery.data.data : null; + + const loadFiles = async () => { + if (!agentId || !agent?.board_id) return; + setLoading(true); + setError(null); + try { + const response = await fetch( + `/api/v1/agent/boards/${agent.board_id}/agents/${agentId}/files`, + { + headers: { + "Content-Type": "application/json", + }, + } + ); + if (!response.ok) { + throw new Error(`Failed to load files: ${response.statusText}`); + } + const data = await response.json(); + setFiles(data); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to load files"); + } finally { + setLoading(false); + } + }; + + const handleFileClick = async (fileName: string) => { + if (!agentId || !agent?.board_id) return; + setLoading(true); + setError(null); + try { + const response = await fetch( + `/api/v1/agent/boards/${agent.board_id}/agents/${agentId}/files/${fileName}`, + { + headers: { + "Content-Type": "application/json", + }, + } + ); + if (!response.ok) { + throw new Error(`Failed to load file: ${response.statusText}`); + } + const content = await response.text(); + setSelectedFile(fileName); + setFileContent(content); + setEditDialogOpen(true); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to load file"); + } finally { + setLoading(false); + } + }; + + const handleSaveFile = async () => { + if (!agentId || !agent?.board_id || !selectedFile) return; + setLoading(true); + setError(null); + try { + const response = await fetch( + `/api/v1/agent/boards/${agent.board_id}/agents/${agentId}/files/${selectedFile}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + content: fileContent, + reason: "Updated via Mission Control UI", + }), + } + ); + if (!response.ok) { + throw new Error(`Failed to save file: ${response.statusText}`); + } + setEditDialogOpen(false); + setSelectedFile(null); + setFileContent(""); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to save file"); + } finally { + setLoading(false); + } + }; + + const handleImportFile = async () => { + if (!agentId || !agent?.board_id || !importFileName) return; + setLoading(true); + setError(null); + try { + const response = await fetch( + `/api/v1/agent/boards/${agent.board_id}/agents/${agentId}/files/${importFileName}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + content: importFileContent, + reason: "Imported via Mission Control UI", + }), + } + ); + if (!response.ok) { + throw new Error(`Failed to import file: ${response.statusText}`); + } + setImportDialogOpen(false); + setImportFileName(""); + setImportFileContent(""); + await loadFiles(); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to import file"); + } finally { + setLoading(false); + } + }; + + // Load files when component mounts + useEffect(() => { + if (agent?.board_id) { + void loadFiles(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [agent?.board_id]); + + return ( + +
+
+
+

Agent Files

+

+ View and edit agent configuration files +

+
+
+ + +
+
+ + {error ? ( +
+ {error} +
+ ) : null} + + {loading && files.length === 0 ? ( +
+ Loading files… +
+ ) : ( +
+
+ {files.length === 0 ? ( +
+

No files found

+

+ Agent files will appear here once provisioned +

+
+ ) : ( + files.map((file) => ( +
+
+

{file.name}

+

+ {file.editable ? "Editable" : "Read-only"} +

+
+
+ +
+
+ )) + )} +
+
+ )} +
+ + {/* Edit/View Dialog */} + + + + + {selectedFile && + files.find((f) => f.name === selectedFile)?.editable + ? "Edit" + : "View"}{" "} + {selectedFile} + + + {selectedFile && + files.find((f) => f.name === selectedFile)?.editable + ? "Make changes to the file content below" + : "This file is read-only"} + + +
+