7 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
9816a43f91 fix: remove unnecessary directive and reduce code duplication
Co-authored-by: abhi1693 <5083532+abhi1693@users.noreply.github.com>
2026-02-21 03:02:52 +00:00
copilot-swe-agent[bot]
8c197ffbbd refactor: address final code review comments
Co-authored-by: abhi1693 <5083532+abhi1693@users.noreply.github.com>
2026-02-21 03:01:45 +00:00
copilot-swe-agent[bot]
23fe9b869d refactor: remove unused schema and clarify SOUL.md endpoint usage
Co-authored-by: abhi1693 <5083532+abhi1693@users.noreply.github.com>
2026-02-21 02:59:31 +00:00
copilot-swe-agent[bot]
d4f519c580 fix: address code review comments
Co-authored-by: abhi1693 <5083532+abhi1693@users.noreply.github.com>
2026-02-21 02:58:27 +00:00
copilot-swe-agent[bot]
e99b41fa9a feat: add frontend UI for agent file management
Co-authored-by: abhi1693 <5083532+abhi1693@users.noreply.github.com>
2026-02-21 02:57:04 +00:00
copilot-swe-agent[bot]
d72af04170 feat: add backend API endpoints for agent file import and editing
Co-authored-by: abhi1693 <5083532+abhi1693@users.noreply.github.com>
2026-02-21 02:53:55 +00:00
copilot-swe-agent[bot]
a8ab696a35 Initial plan 2026-02-21 02:49:02 +00:00
6 changed files with 938 additions and 0 deletions

View File

@@ -24,6 +24,7 @@ from app.models.boards import Board
from app.models.tags import Tag
from app.models.task_dependencies import TaskDependency
from app.models.tasks import Task
from app.schemas.agent_files import AgentFileUpdate
from app.schemas.agents import (
AgentCreate,
AgentHeartbeat,
@@ -1538,6 +1539,190 @@ async def update_agent_soul(
return OkResponse()
@router.get(
"/boards/{board_id}/agents/{agent_id}/files",
response_model=list[dict[str, object]],
tags=AGENT_BOARD_TAGS,
summary="List agent files",
description="List available agent markdown files from the gateway workspace.",
operation_id="agent_board_list_files",
openapi_extra=_agent_board_openapi_hints(
intent="agent_files_list",
when_to_use=[
"Discover available agent files before reading",
"Check which files can be edited",
],
routing_examples=[
{
"input": {
"intent": "list agent configuration files",
"required_privilege": "board_lead_or_same_actor",
},
"decision": "agent_board_list_files",
}
],
side_effects=["No persisted side effects"],
),
)
async def list_agent_files(
agent_id: str,
board: Board = BOARD_DEP,
session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> list[dict[str, object]]:
"""List available agent files.
Allowed for board lead or for an agent listing its own files.
"""
_guard_board_access(agent_ctx, board)
OpenClawAuthorizationPolicy.require_board_lead_or_same_actor(
actor_agent=agent_ctx.agent,
target_agent_id=agent_id,
)
coordination = GatewayCoordinationService(session)
return await coordination.list_agent_files(
board=board,
target_agent_id=agent_id,
correlation_id=f"files.list:{board.id}:{agent_id}",
)
@router.get(
"/boards/{board_id}/agents/{agent_id}/files/{filename}",
response_model=str,
tags=AGENT_BOARD_TAGS,
summary="Read agent file",
description="Fetch content of a specific agent markdown file.",
operation_id="agent_board_read_file",
openapi_extra=_agent_board_openapi_hints(
intent="agent_file_read",
when_to_use=[
"Read IDENTITY.md, BOOTSTRAP.md, or other agent configuration files",
"Review current agent file content before editing",
],
routing_examples=[
{
"input": {
"intent": "read agent configuration file",
"required_privilege": "board_lead_or_same_actor",
},
"decision": "agent_board_read_file",
}
],
side_effects=["No persisted side effects"],
),
)
async def read_agent_file(
agent_id: str,
filename: str,
board: Board = BOARD_DEP,
session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> str:
"""Read content of an agent file.
Allowed for board lead or for an agent reading its own files.
"""
_guard_board_access(agent_ctx, board)
OpenClawAuthorizationPolicy.require_board_lead_or_same_actor(
actor_agent=agent_ctx.agent,
target_agent_id=agent_id,
)
coordination = GatewayCoordinationService(session)
return await coordination.get_agent_file(
board=board,
target_agent_id=agent_id,
filename=filename,
correlation_id=f"file.read:{board.id}:{agent_id}:{filename}",
)
@router.put(
"/boards/{board_id}/agents/{agent_id}/files/{filename}",
response_model=OkResponse,
tags=AGENT_LEAD_TAGS,
summary="Update agent file",
description=(
"Write content to an agent markdown file and persist it for reprovisioning.\n\n"
"Use this when agent configuration files need updates. While this endpoint can "
"handle SOUL.md, prefer the dedicated SOUL endpoint for better semantic clarity "
"and specialized SOUL operations."
),
operation_id="agent_lead_update_file",
responses={
200: {"description": "File updated"},
403: {
"model": LLMErrorResponse,
"description": "Caller is not board lead",
},
404: {
"model": LLMErrorResponse,
"description": "Board or target agent not found",
},
422: {
"model": LLMErrorResponse,
"description": "File content is invalid or empty",
},
502: {
"model": LLMErrorResponse,
"description": "Gateway sync failed",
},
},
openapi_extra={
"x-llm-intent": "agent_file_authoring",
"x-when-to-use": [
"Update agent configuration files like IDENTITY.md, BOOTSTRAP.md, or AGENTS.md",
"Import existing agent files into mission control",
],
"x-when-not-to-use": [
"Prefer dedicated SOUL endpoint for SOUL.md (though this endpoint can handle it)",
"Use task comments for transient guidance",
],
"x-required-actor": "board_lead",
"x-prerequisites": [
"Authenticated board lead",
"Non-empty file content",
"Target agent scoped to board",
],
"x-side-effects": [
"Updates file content in gateway workspace",
"Persists to database for certain files (IDENTITY.md, SOUL.md)",
"Creates activity log entry",
],
"x-routing-policy": [
"Use when updating agent configuration files",
"Prefer dedicated SOUL endpoint for SOUL.md",
],
},
)
async def update_agent_file(
agent_id: str,
filename: str,
payload: AgentFileUpdate,
board: Board = BOARD_DEP,
session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> OkResponse:
"""Update an agent file in gateway and optionally persist to DB.
Lead-only endpoint. Persists IDENTITY.md and SOUL.md for reprovisioning.
"""
_guard_board_access(agent_ctx, board)
_require_board_lead(agent_ctx)
coordination = GatewayCoordinationService(session)
await coordination.update_agent_file(
board=board,
target_agent_id=agent_id,
filename=filename,
content=payload.content,
reason=payload.reason,
actor_agent_id=agent_ctx.agent.id,
correlation_id=f"file.write:{board.id}:{agent_id}:{filename}",
)
return OkResponse()
@router.delete(
"/boards/{board_id}/agents/{agent_id}",
response_model=OkResponse,

View File

@@ -0,0 +1,73 @@
"""Pydantic schemas for agent file operations."""
from __future__ import annotations
from pydantic import Field
from sqlmodel import SQLModel
from sqlmodel._compat import SQLModelConfig
class AgentFileRead(SQLModel):
"""Response model for reading an agent file."""
model_config = SQLModelConfig(
json_schema_extra={
"x-llm-intent": "agent_file_content",
"x-when-to-use": [
"Retrieve content of an agent markdown file",
"Read IDENTITY.md, SOUL.md, BOOTSTRAP.md, or other agent files",
],
},
)
name: str = Field(
description="File name (e.g., IDENTITY.md, SOUL.md, BOOTSTRAP.md)",
examples=["IDENTITY.md", "SOUL.md"],
)
content: str = Field(
description="File content",
examples=["# IDENTITY.md\n\n## Core\n- Name: Agent Name"],
)
class AgentFileUpdate(SQLModel):
"""Request model for updating an agent file."""
model_config = SQLModelConfig(
json_schema_extra={
"x-llm-intent": "agent_file_update",
"x-when-to-use": [
"Update an agent markdown file",
"Modify IDENTITY.md or other editable agent files",
],
},
)
content: str = Field(
description="New file content",
examples=["# IDENTITY.md\n\n## Core\n- Name: Updated Agent Name"],
)
reason: str | None = Field(
default=None,
description="Optional reason for the update",
examples=["Updated agent role and communication style"],
)
class AgentFileListItem(SQLModel):
"""Agent file list item."""
model_config = SQLModelConfig(
json_schema_extra={
"x-llm-intent": "agent_file_list_item",
},
)
name: str = Field(
description="File name",
examples=["IDENTITY.md", "SOUL.md", "BOOTSTRAP.md"],
)
editable: bool = Field(
description="Whether the file can be edited via the API",
examples=[True, False],
)

View File

@@ -49,6 +49,16 @@ from app.services.openclaw.shared import GatewayAgentIdentity
_T = TypeVar("_T")
# Files that can be edited via the agent file management API
EDITABLE_AGENT_FILES = {
"IDENTITY.md",
"SOUL.md",
"BOOTSTRAP.md",
"AGENTS.md",
"TOOLS.md",
"HEARTBEAT.md",
}
class AbstractGatewayMessagingService(OpenClawDBService, ABC):
"""Shared gateway messaging primitives with retry semantics."""
@@ -408,6 +418,275 @@ class GatewayCoordinationService(AbstractGatewayMessagingService):
actor_agent_id,
)
async def list_agent_files(
self,
*,
board: Board,
target_agent_id: str,
correlation_id: str | None = None,
) -> list[dict[str, object]]:
"""List available agent files from the gateway."""
trace_id = GatewayDispatchService.resolve_trace_id(
correlation_id, prefix="coord.files.list"
)
self.logger.log(
TRACE_LEVEL,
"gateway.coordination.files_list.start trace_id=%s board_id=%s target_agent_id=%s",
trace_id,
board.id,
target_agent_id,
)
target = await self._board_agent_or_404(board=board, agent_id=target_agent_id)
_gateway, config = await GatewayDispatchService(
self.session
).require_gateway_config_for_board(board)
try:
async def _do_list() -> object:
return await openclaw_call(
"agents.files.list",
{"agentId": agent_key(target)},
config=config,
)
payload = await self._with_gateway_retry(_do_list)
except (OpenClawGatewayError, TimeoutError) as exc:
self.logger.error(
"gateway.coordination.files_list.failed trace_id=%s board_id=%s "
"target_agent_id=%s error=%s",
trace_id,
board.id,
target_agent_id,
str(exc),
)
raise map_gateway_error_to_http_exception(GatewayOperation.FILES_LIST, exc) from exc
except Exception as exc: # pragma: no cover - defensive guard
self.logger.critical(
"gateway.coordination.files_list.failed_unexpected trace_id=%s board_id=%s "
"target_agent_id=%s error_type=%s error=%s",
trace_id,
board.id,
target_agent_id,
exc.__class__.__name__,
str(exc),
)
raise
# Parse the file list from gateway response
if isinstance(payload, dict):
files = payload.get("files", [])
elif isinstance(payload, list):
files = payload
else:
files = []
result = []
if isinstance(files, list):
for file in files:
if isinstance(file, str):
result.append({"name": file, "editable": file in EDITABLE_AGENT_FILES})
elif isinstance(file, dict):
name = file.get("name", "")
if isinstance(name, str):
result.append(
{"name": name, "editable": name in EDITABLE_AGENT_FILES}
)
self.logger.info(
"gateway.coordination.files_list.success trace_id=%s board_id=%s target_agent_id=%s "
"file_count=%s",
trace_id,
board.id,
target_agent_id,
len(result),
)
return result
async def get_agent_file(
self,
*,
board: Board,
target_agent_id: str,
filename: str,
correlation_id: str | None = None,
) -> str:
"""Get content of an agent file from the gateway."""
trace_id = GatewayDispatchService.resolve_trace_id(
correlation_id, prefix="coord.file.read"
)
self.logger.log(
TRACE_LEVEL,
"gateway.coordination.file_read.start trace_id=%s board_id=%s target_agent_id=%s "
"filename=%s",
trace_id,
board.id,
target_agent_id,
filename,
)
target = await self._board_agent_or_404(board=board, agent_id=target_agent_id)
_gateway, config = await GatewayDispatchService(
self.session
).require_gateway_config_for_board(board)
try:
async def _do_get() -> object:
return await openclaw_call(
"agents.files.get",
{"agentId": agent_key(target), "name": filename},
config=config,
)
payload = await self._with_gateway_retry(_do_get)
except (OpenClawGatewayError, TimeoutError) as exc:
self.logger.error(
"gateway.coordination.file_read.failed trace_id=%s board_id=%s "
"target_agent_id=%s filename=%s error=%s",
trace_id,
board.id,
target_agent_id,
filename,
str(exc),
)
raise map_gateway_error_to_http_exception(GatewayOperation.FILE_READ, exc) from exc
except Exception as exc: # pragma: no cover - defensive guard
self.logger.critical(
"gateway.coordination.file_read.failed_unexpected trace_id=%s board_id=%s "
"target_agent_id=%s filename=%s error_type=%s error=%s",
trace_id,
board.id,
target_agent_id,
filename,
exc.__class__.__name__,
str(exc),
)
raise
content = self._gateway_file_content(payload)
if content is None:
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail="Invalid gateway response",
)
self.logger.info(
"gateway.coordination.file_read.success trace_id=%s board_id=%s target_agent_id=%s "
"filename=%s",
trace_id,
board.id,
target_agent_id,
filename,
)
return content
async def update_agent_file(
self,
*,
board: Board,
target_agent_id: str,
filename: str,
content: str,
reason: str | None,
actor_agent_id: UUID,
correlation_id: str | None = None,
) -> None:
"""Update an agent file in the gateway and optionally persist to DB."""
trace_id = GatewayDispatchService.resolve_trace_id(
correlation_id, prefix="coord.file.write"
)
self.logger.log(
TRACE_LEVEL,
"gateway.coordination.file_write.start trace_id=%s board_id=%s target_agent_id=%s "
"filename=%s actor_agent_id=%s",
trace_id,
board.id,
target_agent_id,
filename,
actor_agent_id,
)
target = await self._board_agent_or_404(board=board, agent_id=target_agent_id)
normalized_content = content.strip()
if not normalized_content:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail="content is required",
)
# Update database fields for specific files
db_updated = False
if filename == "SOUL.md":
target.soul_template = normalized_content
db_updated = True
elif filename == "IDENTITY.md":
target.identity_template = normalized_content
db_updated = True
if db_updated:
target.updated_at = utcnow()
self.session.add(target)
await self.session.commit()
_gateway, config = await GatewayDispatchService(
self.session
).require_gateway_config_for_board(board)
try:
async def _do_set() -> object:
return await openclaw_call(
"agents.files.set",
{
"agentId": agent_key(target),
"name": filename,
"content": normalized_content,
},
config=config,
)
await self._with_gateway_retry(_do_set)
except (OpenClawGatewayError, TimeoutError) as exc:
self.logger.error(
"gateway.coordination.file_write.failed trace_id=%s board_id=%s "
"target_agent_id=%s filename=%s actor_agent_id=%s error=%s",
trace_id,
board.id,
target_agent_id,
filename,
actor_agent_id,
str(exc),
)
raise map_gateway_error_to_http_exception(GatewayOperation.FILE_WRITE, exc) from exc
except Exception as exc: # pragma: no cover - defensive guard
self.logger.critical(
"gateway.coordination.file_write.failed_unexpected trace_id=%s board_id=%s "
"target_agent_id=%s filename=%s actor_agent_id=%s error_type=%s error=%s",
trace_id,
board.id,
target_agent_id,
filename,
actor_agent_id,
exc.__class__.__name__,
str(exc),
)
raise
reason_text = (reason or "").strip()
note = f"{filename} updated for {target.name}."
if reason_text:
note = f"{note} Reason: {reason_text}"
record_activity(
self.session,
event_type="agent.file.updated",
message=note,
agent_id=actor_agent_id,
)
await self.session.commit()
self.logger.info(
"gateway.coordination.file_write.success trace_id=%s board_id=%s target_agent_id=%s "
"filename=%s actor_agent_id=%s",
trace_id,
board.id,
target_agent_id,
filename,
actor_agent_id,
)
async def ask_user_via_gateway_main(
self,
*,

View File

@@ -14,6 +14,9 @@ class GatewayOperation(str, Enum):
NUDGE_AGENT = "nudge_agent"
SOUL_READ = "soul_read"
SOUL_WRITE = "soul_write"
FILES_LIST = "files_list"
FILE_READ = "file_read"
FILE_WRITE = "file_write"
ASK_USER_DISPATCH = "ask_user_dispatch"
LEAD_MESSAGE_DISPATCH = "lead_message_dispatch"
LEAD_BROADCAST_DISPATCH = "lead_broadcast_dispatch"
@@ -42,6 +45,18 @@ _GATEWAY_ERROR_POLICIES: dict[GatewayOperation, GatewayErrorPolicy] = {
status_code=status.HTTP_502_BAD_GATEWAY,
detail_template="Gateway SOUL update failed: {error}",
),
GatewayOperation.FILES_LIST: GatewayErrorPolicy(
status_code=status.HTTP_502_BAD_GATEWAY,
detail_template="Gateway files list failed: {error}",
),
GatewayOperation.FILE_READ: GatewayErrorPolicy(
status_code=status.HTTP_502_BAD_GATEWAY,
detail_template="Gateway file read failed: {error}",
),
GatewayOperation.FILE_WRITE: GatewayErrorPolicy(
status_code=status.HTTP_502_BAD_GATEWAY,
detail_template="Gateway file update failed: {error}",
),
GatewayOperation.ASK_USER_DISPATCH: GatewayErrorPolicy(
status_code=status.HTTP_502_BAD_GATEWAY,
detail_template="Gateway ask-user dispatch failed: {error}",

View File

@@ -0,0 +1,378 @@
"use client";
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";
type AgentFile = {
name: string;
editable: boolean;
};
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<AgentFile[]>([]);
const [selectedFile, setSelectedFile] = useState<string | null>(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<string | null>(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: {
Accept: "text/plain",
},
}
);
if (!response.ok) {
throw new Error(`Failed to load file: ${response.statusText}`);
}
const fileContent = await response.text();
setSelectedFile(fileName);
setFileContent(fileContent);
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 (
<DashboardPageLayout
signedOut={{
message: "Sign in to manage agent files.",
forceRedirectUrl: `/agents/${agentId}/files`,
signUpForceRedirectUrl: `/agents/${agentId}/files`,
}}
title={agent?.name ? `${agent.name} - Files` : "Agent Files"}
description="Manage and edit agent markdown files"
isAdmin={isAdmin}
adminOnlyMessage="Only organization owners and admins can manage agent files."
>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-slate-900">Agent Files</h2>
<p className="mt-1 text-sm text-slate-600">
View and edit agent configuration files
</p>
</div>
<div className="flex gap-3">
<Button onClick={() => setImportDialogOpen(true)}>
Import File
</Button>
<Button
variant="outline"
onClick={() => router.push(`/agents/${agentId}`)}
>
Back to Agent
</Button>
</div>
</div>
{error ? (
<div className="rounded-lg border border-slate-200 bg-red-50 p-3 text-sm text-red-600">
{error}
</div>
) : null}
{loading && files.length === 0 ? (
<div className="flex items-center justify-center py-12 text-sm text-slate-600">
Loading files
</div>
) : (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="divide-y divide-slate-200">
{files.length === 0 ? (
<div className="p-8 text-center">
<p className="text-sm text-slate-600">No files found</p>
<p className="mt-1 text-xs text-slate-500">
Agent files will appear here once provisioned
</p>
</div>
) : (
files.map((file) => (
<div
key={file.name}
className="flex items-center justify-between p-4 hover:bg-slate-50"
>
<div>
<p className="font-medium text-slate-900">{file.name}</p>
<p className="text-xs text-slate-500">
{file.editable ? "Editable" : "Read-only"}
</p>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => void handleFileClick(file.name)}
>
{file.editable ? "Edit" : "View"}
</Button>
</div>
</div>
))
)}
</div>
</div>
)}
</div>
{/* Edit/View Dialog */}
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>
{selectedFile &&
files.find((f) => f.name === selectedFile)?.editable
? "Edit"
: "View"}{" "}
{selectedFile}
</DialogTitle>
<DialogDescription>
{selectedFile &&
files.find((f) => f.name === selectedFile)?.editable
? "Make changes to the file content below"
: "This file is read-only"}
</DialogDescription>
</DialogHeader>
<div className="py-4">
<Textarea
value={fileContent}
onChange={(e) => setFileContent(e.target.value)}
className="min-h-[400px] font-mono text-sm"
disabled={
!selectedFile ||
!files.find((f) => f.name === selectedFile)?.editable
}
/>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setEditDialogOpen(false);
setSelectedFile(null);
setFileContent("");
}}
>
Cancel
</Button>
{selectedFile &&
files.find((f) => f.name === selectedFile)?.editable ? (
<Button onClick={() => void handleSaveFile()} disabled={loading}>
{loading ? "Saving…" : "Save Changes"}
</Button>
) : null}
</DialogFooter>
</DialogContent>
</Dialog>
{/* Import Dialog */}
<Dialog open={importDialogOpen} onOpenChange={setImportDialogOpen}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>Import Agent File</DialogTitle>
<DialogDescription>
Upload an existing agent markdown file to Mission Control
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-900">
File name <span className="text-red-500">*</span>
</label>
<Input
value={importFileName}
onChange={(e) => setImportFileName(e.target.value)}
placeholder="e.g., IDENTITY.md, SOUL.md, BOOTSTRAP.md"
/>
<p className="text-xs text-slate-500">
Use standard OpenClaw file names (IDENTITY.md, SOUL.md, etc.)
</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-900">
File content <span className="text-red-500">*</span>
</label>
<Textarea
value={importFileContent}
onChange={(e) => setImportFileContent(e.target.value)}
className="min-h-[400px] font-mono text-sm"
placeholder="Paste your agent file content here..."
/>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setImportDialogOpen(false);
setImportFileName("");
setImportFileContent("");
}}
>
Cancel
</Button>
<Button
onClick={() => void handleImportFile()}
disabled={loading || !importFileName || !importFileContent}
>
{loading ? "Importing…" : "Import File"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DashboardPageLayout>
);
}

View File

@@ -196,6 +196,14 @@ export default function AgentDetailPage() {
Edit
</Link>
) : null}
{agent ? (
<Link
href={`/agents/${agent.id}/files`}
className="inline-flex h-10 items-center justify-center rounded-xl border border-[color:var(--border)] px-4 text-sm font-semibold text-muted transition hover:border-[color:var(--accent)] hover:text-[color:var(--accent)]"
>
Files
</Link>
) : null}
{agent ? (
<Button variant="outline" onClick={() => setDeleteOpen(true)}>
Delete