Skip to content
Open
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pnpm-lock.yaml - Project file
pnpm-workspace.yaml - Project file
PRD.md - Project file
REFACTOR_TASK.md - Project file
client/AGENTS.md - Client module map
server/AGENTS.md - Server module map
tsconfig.json - Project file
web/AGENTS.md - Web module map
Expand Down
32 changes: 32 additions & 0 deletions client/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# client/
> L2 | 父级: /AGENTS.md

成员清单
package.json: Client package manifest with React/Vite/Tailwind dependencies and scripts.
vite.config.ts: Vite config with React plugin, Tailwind plugin, alias, and backend proxies.
tailwind.config.ts: Tailwind theme + dark mode class strategy.
tsconfig.json: TypeScript compiler settings for client source and config files.
index.html: App HTML shell with Inter font preload and root mount node.
postcss.config.cjs: PostCSS autoprefixer compatibility hook.
src/: Client runtime source tree for app shell, pages, stores, hooks, and styles.

目录结构
src/
├── AGENTS.md
├── App.tsx
├── main.tsx
├── types.ts
├── api/
├── components/
├── hooks/
├── lib/
├── pages/
├── stores/
└── styles/

法则
- Keep task and agent state in Zustand stores; do not duplicate domain state in page-local caches.
- Route all backend calls through `src/api/client.ts`.
- Realtime updates must flow through `useWebSocket` into stores.

[PROTOCOL]: 变更时更新此头部,然后检查 AGENTS.md
18 changes: 18 additions & 0 deletions client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<title>AgentCal Client</title>
</head>
<body class="font-sans antialiased">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
27 changes: 26 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
{
"name": "@agentcal/client",
"version": "0.1.0",
"private": true
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.30.0",
"zustand": "^4.5.7",
"recharts": "^2.15.4"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.12",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.7.0",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.3",
"tailwindcss": "^4.1.12",
"typescript": "^5.9.2",
"vite": "^5.4.19"
}
}
5 changes: 5 additions & 0 deletions client/postcss.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {},
},
};
23 changes: 23 additions & 0 deletions client/src/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# src/
> L2 | 父级: /client/AGENTS.md

成员清单
main.tsx: Browser entrypoint and root render bootstrap.
App.tsx: Route registration and websocket bootstrap.
types.ts: Shared domain types aligned to backend schemas.
api/client.ts: Typed REST wrappers for tasks/agents/calendar/system.
stores/taskStore.ts: Task state, calendar state, selected task, and live logs.
stores/agentStore.ts: Agent list state and upsert helpers.
hooks/useWebSocket.ts: Reconnecting websocket event dispatcher.
hooks/useCalendar.ts: Calendar date/view navigation helpers.
components/: Reusable layout and UI primitives.
pages/: Route-level feature pages.
lib/status.ts: Task status label/color presentation helpers.
styles/globals.css: Tailwind import and global visual tokens.

法则
- `types.ts` is the only shared type source for app modules.
- Keep page components thin; move reusable behavior to hooks/components/stores.
- Keep visual semantics centralized in `lib/status.ts`.

[PROTOCOL]: 变更时更新此头部,然后检查 AGENTS.md
32 changes: 32 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* [INPUT]: Depends on react-router route tree, layout shell, pages, and websocket lifecycle hook.
* [OUTPUT]: Registers application routes and global realtime subscription bootstrap.
* [POS]: root client composition entry wiring navigation and page surfaces.
* [PROTOCOL]: 变更时更新此头部,然后检查 AGENTS.md
*/
import { Navigate, Route, Routes } from "react-router-dom";
import { Layout } from "@/components/Layout";
import { useWebSocket } from "@/hooks/useWebSocket";
import { AgentsPage } from "@/pages/AgentsPage";
import { CalendarPage } from "@/pages/CalendarPage";
import { SettingsPage } from "@/pages/SettingsPage";
import { StatsPage } from "@/pages/StatsPage";
import { TaskDetailPage } from "@/pages/TaskDetailPage";

export default function App() {
useWebSocket();

return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Navigate to="/calendar" replace />} />
<Route path="calendar" element={<CalendarPage />} />
<Route path="agents" element={<AgentsPage />} />
<Route path="stats" element={<StatsPage />} />
<Route path="settings" element={<SettingsPage />} />
<Route path="tasks/:taskId" element={<TaskDetailPage />} />
</Route>
<Route path="*" element={<Navigate to="/calendar" replace />} />
</Routes>
);
}
11 changes: 11 additions & 0 deletions client/src/api/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# api/
> L2 | 父级: /client/src/AGENTS.md

成员清单
client.ts: REST client boundary with typed request helpers and endpoint wrappers.

法则
- Add new endpoints here first, then consume from stores/pages.
- Keep all request errors normalized with consistent Error messages.

[PROTOCOL]: 变更时更新此头部,然后检查 AGENTS.md
200 changes: 200 additions & 0 deletions client/src/api/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/**
* [INPUT]: Depends on browser fetch and shared domain types from client/src/types.
* [OUTPUT]: Exposes typed wrappers for agents/tasks/calendar/system REST endpoints.
* [POS]: client network boundary that normalizes request/response handling for stores and pages.
* [PROTOCOL]: 变更时更新此头部,然后检查 AGENTS.md
*/
import type {
Agent,
CalendarDailyResponse,
CalendarMonthlyResponse,
CalendarWeeklyResponse,
PromptTaskFromPromptResponse,
QueueStatus,
SystemConfig,
SystemStats,
SystemStatus,
Task,
} from "@/types";

const API_BASE = "/api";

async function request<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(`${API_BASE}${path}`, {
headers: {
"Content-Type": "application/json",
...(init?.headers ?? {}),
},
...init,
});

if (!response.ok) {
const payload = await response.text();
throw new Error(`API ${response.status}: ${payload || response.statusText}`);
}

if (response.status === 204) {
return undefined as T;
}

return (await response.json()) as T;
}

function withQuery(path: string, query: Record<string, string | undefined>): string {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(query)) {
if (value !== undefined && value !== "") {
params.set(key, value);
}
}
const suffix = params.toString();
return suffix.length > 0 ? `${path}?${suffix}` : path;
}

export interface CreateTaskPayload {
title: string;
description?: string;
status?: Task["status"];
priority?: Task["priority"];
agent_type?: Task["agent_type"];
agent_id?: string | null;
scheduled_at?: string | null;
estimated_duration_min?: number;
depends_on?: string[];
}

export type UpdateTaskPayload = Partial<Omit<Task, "id" | "project_id" | "created_at" | "updated_at">>;

export async function fetchAgents(projectId?: string): Promise<Agent[]> {
return request<Agent[]>(withQuery("/agents", { project_id: projectId }));
}

export async function fetchAgent(id: string): Promise<Agent> {
return request<Agent>(`/agents/${id}`);
}

export async function createAgent(payload: Partial<Agent>): Promise<Agent> {
return request<Agent>("/agents", {
method: "POST",
body: JSON.stringify(payload),
});
}

export async function updateAgent(id: string, payload: Partial<Agent>): Promise<Agent> {
return request<Agent>(`/agents/${id}`, {
method: "PUT",
body: JSON.stringify(payload),
});
}

export async function deleteAgent(id: string): Promise<void> {
await request<void>(`/agents/${id}`, { method: "DELETE" });
}

export async function fetchTasks(params?: Record<string, string>): Promise<Task[]> {
return request<Task[]>(withQuery("/tasks", params ?? {}));
}

export async function fetchTask(id: string): Promise<Task> {
return request<Task>(`/tasks/${id}`);
}

export async function createTask(payload: CreateTaskPayload): Promise<Task> {
return request<Task>("/tasks", {
method: "POST",
body: JSON.stringify(payload),
});
}

export async function updateTask(id: string, payload: UpdateTaskPayload): Promise<Task> {
return request<Task>(`/tasks/${id}`, {
method: "PUT",
body: JSON.stringify(payload),
});
}

export async function deleteTask(id: string): Promise<void> {
await request<void>(`/tasks/${id}`, { method: "DELETE" });
}

export async function spawnTask(id: string): Promise<Task> {
const response = await request<{ task: Task }>(`/tasks/${id}/spawn`, {
method: "POST",
});
return response.task;
}

export async function redirectTask(id: string, message: string): Promise<{ ok: boolean }> {
return request<{ ok: boolean }>(`/tasks/${id}/redirect`, {
method: "POST",
body: JSON.stringify({ message }),
});
}

export async function killTask(id: string): Promise<Task> {
const response = await request<{ task: Task }>(`/tasks/${id}/kill`, {
method: "POST",
});
return response.task;
}

export async function retryTask(id: string): Promise<Task> {
return request<Task>(`/tasks/${id}/retry`, {
method: "POST",
});
}

export async function parseTaskFromPrompt(prompt: string): Promise<PromptTaskFromPromptResponse> {
return request<PromptTaskFromPromptResponse>("/tasks/from-prompt", {
method: "POST",
body: JSON.stringify({ prompt, dry_run: true }),
});
}

export async function createTaskFromPrompt(prompt: string): Promise<PromptTaskFromPromptResponse> {
return request<PromptTaskFromPromptResponse>("/tasks/from-prompt", {
method: "POST",
body: JSON.stringify({ prompt, dry_run: false }),
});
}

export async function fetchCalendarDay(date: string): Promise<CalendarDailyResponse> {
return request<CalendarDailyResponse>(withQuery("/calendar/daily", { date }));
}

export async function fetchCalendarWeek(date: string): Promise<CalendarWeeklyResponse> {
return request<CalendarWeeklyResponse>(withQuery("/calendar/weekly", { date }));
}

export async function fetchCalendarMonth(date: string): Promise<CalendarMonthlyResponse> {
return request<CalendarMonthlyResponse>(withQuery("/calendar/monthly", { date }));
}

export async function fetchSystemStats(): Promise<SystemStats> {
return request<SystemStats>("/system/stats");
}

export async function fetchSystemStatus(): Promise<SystemStatus> {
return request<SystemStatus>("/system/status");
}

export async function fetchSystemQueue(): Promise<QueueStatus> {
return request<QueueStatus>("/system/queue");
}

export async function fetchSystemConfig(): Promise<SystemConfig> {
return request<SystemConfig>("/system/config");
}

export async function updateSystemConfig(payload: Partial<SystemConfig>): Promise<SystemConfig> {
return request<SystemConfig>("/system/config", {
method: "PUT",
body: JSON.stringify(payload),
});
}

export async function triggerSystemSync(): Promise<{ synced_at: string }> {
return request<{ synced_at: string }>("/system/sync", {
method: "POST",
});
}
16 changes: 16 additions & 0 deletions client/src/components/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# components/
> L2 | 父级: /client/src/AGENTS.md

成员清单
Layout.tsx: Shell wrapper with sidebar and main outlet.
Sidebar.tsx: Collapsible navigation with route links and theme switch.
ThemeToggle.tsx: Theme toggle control.
CalendarGrid.tsx: Custom day/week/month scheduling grid.
TaskCard.tsx: Status-colored task block renderer.
LogViewer.tsx: Auto-scrolling live log panel.

法则
- Keep components presentational; domain mutations should come from stores/pages.
- Preserve 8px radius and subtle shadow for Notion-like consistency.

[PROTOCOL]: 变更时更新此头部,然后检查 AGENTS.md
Loading
Loading