Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 228 additions & 0 deletions interface/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,15 @@ export interface SandboxSection {
writable_paths: string[];
}

export interface ProjectsSection {
use_worktrees: boolean;
worktree_name_template: string;
auto_create_worktrees: boolean;
auto_discover_repos: boolean;
auto_discover_worktrees: boolean;
disk_usage_warning_threshold: number;
}

export interface DiscordSection {
enabled: boolean;
allow_bot_messages: boolean;
Expand All @@ -640,6 +649,7 @@ export interface AgentConfigResponse {
channel: ChannelSection;
discord: DiscordSection;
sandbox: SandboxSection;
projects: ProjectsSection;
}

// Partial update types - all fields are optional
Expand Down Expand Up @@ -713,6 +723,15 @@ export interface SandboxUpdate {
writable_paths?: string[];
}

export interface ProjectsUpdate {
use_worktrees?: boolean;
worktree_name_template?: string;
auto_create_worktrees?: boolean;
auto_discover_repos?: boolean;
auto_discover_worktrees?: boolean;
disk_usage_warning_threshold?: number;
}

export interface DiscordUpdate {
allow_bot_messages?: boolean;
}
Expand All @@ -729,6 +748,7 @@ export interface AgentConfigUpdateRequest {
channel?: ChannelUpdate;
discord?: DiscordUpdate;
sandbox?: SandboxUpdate;
projects?: ProjectsUpdate;
}

// -- Cron Types --
Expand Down Expand Up @@ -1316,6 +1336,121 @@ export interface AgentMessageEvent {
channel_id: string;
}

// ── Projects ─────────────────────────────────────────────────────────────

export type ProjectStatus = "active" | "archived";

export interface Project {
id: string;
agent_id: string;
name: string;
description: string;
icon: string;
tags: string[];
root_path: string;
settings: Record<string, unknown>;
status: ProjectStatus;
created_at: string;
updated_at: string;
}

export interface ProjectRepo {
id: string;
project_id: string;
name: string;
path: string;
remote_url: string;
default_branch: string;
current_branch: string | null;
description: string;
disk_usage_bytes: number | null;
created_at: string;
updated_at: string;
}

export interface ProjectWorktree {
id: string;
project_id: string;
repo_id: string;
name: string;
path: string;
branch: string;
created_by: string;
disk_usage_bytes: number | null;
created_at: string;
updated_at: string;
}

export interface ProjectWorktreeWithRepo extends ProjectWorktree {
repo_name: string;
}

/** GET /agents/projects response */
export interface ProjectListResponse {
projects: Project[];
}

/** GET /agents/projects/:id response — project fields are flattened */
export interface ProjectWithRelations extends Project {
repos: ProjectRepo[];
worktrees: ProjectWorktreeWithRepo[];
}

export interface ProjectDetailResponse {
/** The flattened project + repos + worktrees (serde #[flatten]) */
[key: string]: unknown;
}

export interface ProjectActionResponse {
success: boolean;
message: string;
}

export interface DiskUsageEntry {
name: string;
bytes: number;
is_dir: boolean;
}

export interface DiskUsageResponse {
total_bytes: number;
entries: DiskUsageEntry[];
}

export interface CreateProjectRequest {
name: string;
description?: string;
icon?: string;
tags?: string[];
root_path: string;
settings?: Record<string, unknown>;
auto_discover?: boolean;
}

export interface UpdateProjectRequest {
name?: string;
description?: string;
icon?: string;
tags?: string[];
settings?: Record<string, unknown>;
status?: ProjectStatus;
}

export interface CreateRepoRequest {
name: string;
path: string;
remote_url?: string;
default_branch?: string;
description?: string;
}

export interface CreateWorktreeRequest {
repo_id: string;
branch: string;
worktree_name?: string;
start_point?: string;
}

// ── Secrets ──────────────────────────────────────────────────────────────

export type SecretCategory = "system" | "tool";
Expand Down Expand Up @@ -2136,5 +2271,98 @@ export const api = {
return response.json() as Promise<MigrateResponse>;
},

// Projects API
listProjects: (agentId: string, status?: ProjectStatus) => {
const search = new URLSearchParams({ agent_id: agentId });
if (status) search.set("status", status);
return fetchJson<ProjectListResponse>(`/agents/projects?${search}`);
},

getProject: (agentId: string, projectId: string) =>
fetchJson<ProjectWithRelations>(
`/agents/projects/${encodeURIComponent(projectId)}?agent_id=${encodeURIComponent(agentId)}`,
),

createProject: async (agentId: string, request: CreateProjectRequest): Promise<ProjectWithRelations> => {
const response = await fetch(`${API_BASE}/agents/projects`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...request, agent_id: agentId }),
});
if (!response.ok) throw new Error(`API error: ${response.status}`);
return response.json() as Promise<ProjectWithRelations>;
},

updateProject: async (agentId: string, projectId: string, request: UpdateProjectRequest): Promise<ProjectWithRelations> => {
const response = await fetch(`${API_BASE}/agents/projects/${encodeURIComponent(projectId)}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...request, agent_id: agentId }),
});
if (!response.ok) throw new Error(`API error: ${response.status}`);
return response.json() as Promise<ProjectWithRelations>;
},

deleteProject: async (agentId: string, projectId: string): Promise<ProjectActionResponse> => {
const response = await fetch(
`${API_BASE}/agents/projects/${encodeURIComponent(projectId)}?agent_id=${encodeURIComponent(agentId)}`,
{ method: "DELETE" },
);
if (!response.ok) throw new Error(`API error: ${response.status}`);
return response.json() as Promise<ProjectActionResponse>;
},

scanProject: async (agentId: string, projectId: string): Promise<ProjectWithRelations> => {
const response = await fetch(
`${API_BASE}/agents/projects/${encodeURIComponent(projectId)}/scan?agent_id=${encodeURIComponent(agentId)}`,
{ method: "POST" },
);
if (!response.ok) throw new Error(`API error: ${response.status}`);
return response.json() as Promise<ProjectWithRelations>;
},

projectDiskUsage: (agentId: string, projectId: string) =>
fetchJson<DiskUsageResponse>(
`/agents/projects/${encodeURIComponent(projectId)}/disk-usage?agent_id=${encodeURIComponent(agentId)}`,
),

createProjectRepo: async (agentId: string, projectId: string, request: CreateRepoRequest): Promise<{ repo: ProjectRepo }> => {
const response = await fetch(`${API_BASE}/agents/projects/${encodeURIComponent(projectId)}/repos`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...request, agent_id: agentId }),
});
if (!response.ok) throw new Error(`API error: ${response.status}`);
return response.json() as Promise<{ repo: ProjectRepo }>;
},

deleteProjectRepo: async (agentId: string, projectId: string, repoId: string): Promise<ProjectActionResponse> => {
const response = await fetch(
`${API_BASE}/agents/projects/${encodeURIComponent(projectId)}/repos/${encodeURIComponent(repoId)}?agent_id=${encodeURIComponent(agentId)}`,
{ method: "DELETE" },
);
if (!response.ok) throw new Error(`API error: ${response.status}`);
return response.json() as Promise<ProjectActionResponse>;
},

createProjectWorktree: async (agentId: string, projectId: string, request: CreateWorktreeRequest): Promise<{ worktree: ProjectWorktree }> => {
const response = await fetch(`${API_BASE}/agents/projects/${encodeURIComponent(projectId)}/worktrees`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...request, agent_id: agentId }),
});
if (!response.ok) throw new Error(`API error: ${response.status}`);
return response.json() as Promise<{ worktree: ProjectWorktree }>;
},

deleteProjectWorktree: async (agentId: string, projectId: string, worktreeId: string): Promise<ProjectActionResponse> => {
const response = await fetch(
`${API_BASE}/agents/projects/${encodeURIComponent(projectId)}/worktrees/${encodeURIComponent(worktreeId)}?agent_id=${encodeURIComponent(agentId)}`,
{ method: "DELETE" },
);
if (!response.ok) throw new Error(`API error: ${response.status}`);
return response.json() as Promise<ProjectActionResponse>;
},

eventsUrl: `${API_BASE}/events`,
};
1 change: 1 addition & 0 deletions interface/src/components/AgentTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const tabs = [
{ label: "Memories", to: "/agents/$agentId/memories" as const, exact: false },
{ label: "Ingest", to: "/agents/$agentId/ingest" as const, exact: false },
{ label: "Workers", to: "/agents/$agentId/workers" as const, exact: false },
{ label: "Projects", to: "/agents/$agentId/projects" as const, exact: false },
{ label: "Tasks", to: "/agents/$agentId/tasks" as const, exact: false },
{ label: "Cortex", to: "/agents/$agentId/cortex" as const, exact: false },
{ label: "Skills", to: "/agents/$agentId/skills" as const, exact: false },
Expand Down
18 changes: 18 additions & 0 deletions interface/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {AgentCron} from "@/routes/AgentCron";
import {AgentIngest} from "@/routes/AgentIngest";
import {AgentSkills} from "@/routes/AgentSkills";
import {AgentWorkers} from "@/routes/AgentWorkers";
import {AgentProjects} from "@/routes/AgentProjects";
import {AgentTasks} from "@/routes/AgentTasks";
import {AgentChat} from "@/routes/AgentChat";
import {Settings} from "@/routes/Settings";
Expand Down Expand Up @@ -221,6 +222,22 @@ const agentWorkersRoute = createRoute({
},
});

const agentProjectsRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/agents/$agentId/projects",
component: function AgentProjectsPage() {
const {agentId} = agentProjectsRoute.useParams();
return (
<div className="flex h-full flex-col">
<AgentHeader agentId={agentId} />
<div className="flex-1 overflow-hidden">
<AgentProjects agentId={agentId} />
</div>
</div>
);
},
});

const agentTasksRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/agents/$agentId/tasks",
Expand Down Expand Up @@ -340,6 +357,7 @@ const routeTree = rootRoute.addChildren([
agentMemoriesRoute,
agentIngestRoute,
agentWorkersRoute,
agentProjectsRoute,
agentTasksRoute,
agentCortexRoute,
agentSkillsRoute,
Expand Down
Loading
Loading