diff --git a/README.md b/README.md new file mode 100644 index 00000000..00fad72e --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# Multica + +AI-native task management platform — like Linear, but with AI agents as first-class citizens. + +## Prerequisites + +- [Node.js](https://nodejs.org/) (v20+) +- [pnpm](https://pnpm.io/) (v10.28+) +- [Go](https://go.dev/) (v1.26+) +- [Docker](https://www.docker.com/) + +## Quick Start + +```bash +# 1. Install dependencies +pnpm install + +# 2. Copy environment variables +cp .env.example .env + +# 3. Start PostgreSQL +docker compose up -d + +# 4. Run database migrations +make migrate-up + +# 5. Start the Go backend (port 8080) +make dev + +# 6. In another terminal, start the frontend (port 3000) +pnpm dev:web +``` + +Open [http://localhost:3000](http://localhost:3000) in your browser. + +## Project Structure + +``` +├── server/ # Go backend (Chi + sqlc + gorilla/websocket) +│ ├── cmd/ # server, daemon, migrate, seed +│ ├── internal/ # Core business logic +│ ├── migrations/ # SQL migrations +│ └── sqlc.yaml # sqlc config +├── apps/ +│ └── web/ # Next.js 16 frontend +├── packages/ # Shared TypeScript packages +│ ├── ui/ # Component library (shadcn/ui + Radix) +│ ├── types/ # Shared type definitions +│ ├── sdk/ # API client SDK +│ ├── store/ # State management +│ ├── hooks/ # Shared React hooks +│ └── utils/ # Utility functions +├── Makefile # Backend commands +├── docker-compose.yml # PostgreSQL + pgvector +└── .env.example # Environment variable template +``` + +## Commands + +### Frontend + +| Command | Description | +|---------|-------------| +| `pnpm dev:web` | Start Next.js dev server (port 3000) | +| `pnpm build` | Build all TypeScript packages | +| `pnpm typecheck` | Run TypeScript type checking | +| `pnpm test` | Run TypeScript tests | + +### Backend + +| Command | Description | +|---------|-------------| +| `make dev` | Run Go server (port 8080) | +| `make daemon` | Run local agent daemon | +| `make test` | Run Go tests | +| `make build` | Build server & daemon binaries | +| `make sqlc` | Regenerate sqlc code from SQL | + +### Database + +| Command | Description | +|---------|-------------| +| `docker compose up -d` | Start PostgreSQL | +| `docker compose down` | Stop PostgreSQL | +| `make migrate-up` | Run database migrations | +| `make migrate-down` | Rollback database migrations | +| `make seed` | Seed test data | + +## Environment Variables + +See [`.env.example`](.env.example) for all available variables: + +- `DATABASE_URL` — PostgreSQL connection string +- `PORT` — Backend server port (default: 8080) +- `JWT_SECRET` — JWT signing secret +- `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` — Google OAuth (optional) +- `NEXT_PUBLIC_API_URL` — Frontend → backend API URL +- `NEXT_PUBLIC_WS_URL` — Frontend → backend WebSocket URL diff --git a/apps/web/app/(dashboard)/agents/page.tsx b/apps/web/app/(dashboard)/agents/page.tsx index ccca9ff3..49c7e355 100644 --- a/apps/web/app/(dashboard)/agents/page.tsx +++ b/apps/web/app/(dashboard)/agents/page.tsx @@ -1,15 +1,467 @@ -export default function AgentsPage() { +"use client"; + +import { useState } from "react"; +import { + Bot, + Cloud, + Monitor, + Plus, + Wrench, + Blocks, + Zap, + GitBranch, + FileCode, + MessageSquare, + Terminal, + Database, + Globe, + ListTodo, +} from "lucide-react"; +import type { AgentStatus, AgentRuntimeMode } from "@multica/types"; + +// --------------------------------------------------------------------------- +// Types for mock data +// --------------------------------------------------------------------------- + +interface AgentSkill { + id: string; + name: string; + description: string; +} + +interface AgentTool { + id: string; + name: string; + icon: typeof Terminal; + connected: boolean; +} + +interface AgentTask { + id: string; + issueKey: string; + title: string; + status: "working" | "queued"; +} + +interface MockAgent { + id: string; + name: string; + avatar: string; + runtimeMode: AgentRuntimeMode; + status: AgentStatus; + model: string; + description: string; + maxConcurrentTasks: number; + host?: string; + skills: AgentSkill[]; + tools: AgentTool[]; + currentTasks: AgentTask[]; + completedTasks: number; + createdAt: string; +} + +// --------------------------------------------------------------------------- +// Mock data +// --------------------------------------------------------------------------- + +const MOCK_AGENTS: MockAgent[] = [ + { + id: "agent_1", + name: "Claude-1", + avatar: "C1", + runtimeMode: "local", + status: "working", + model: "Claude Sonnet 4", + description: + "General-purpose coding agent for backend development. Specializes in Go API development, database migrations, and test writing.", + maxConcurrentTasks: 2, + host: "jiayuan-macbook", + skills: [ + { + id: "sk_1", + name: "Go API Development", + description: "Build RESTful APIs with Chi, implement CRUD handlers, add middleware", + }, + { + id: "sk_2", + name: "Database Migrations", + description: "Create and run PostgreSQL migrations, update sqlc queries", + }, + { + id: "sk_3", + name: "Test Writing", + description: "Write Go unit and integration tests with testcontainers", + }, + ], + tools: [ + { id: "t_1", name: "GitHub", icon: GitBranch, connected: true }, + { id: "t_2", name: "Terminal", icon: Terminal, connected: true }, + { id: "t_3", name: "PostgreSQL", icon: Database, connected: true }, + { id: "t_4", name: "Browser", icon: Globe, connected: false }, + ], + currentTasks: [ + { + id: "iss_9", + issueKey: "MUL-9", + title: "Implement issue list API endpoint", + status: "working", + }, + { + id: "iss_14", + issueKey: "MUL-14", + title: "Add WebSocket event types for agent status", + status: "queued", + }, + ], + completedTasks: 12, + createdAt: "2026-03-15T10:00:00Z", + }, + { + id: "agent_2", + name: "Codex-1", + avatar: "CX", + runtimeMode: "cloud", + status: "idle", + model: "GPT-5.3 Codex", + description: + "Cloud-hosted coding agent optimized for frontend development. Handles React components, styling, and TypeScript refactoring.", + maxConcurrentTasks: 4, + skills: [ + { + id: "sk_4", + name: "React Components", + description: "Build UI components with React, Radix UI, and Tailwind CSS", + }, + { + id: "sk_5", + name: "TypeScript Refactoring", + description: "Refactor code for type safety, extract shared types and utilities", + }, + ], + tools: [ + { id: "t_5", name: "GitHub", icon: GitBranch, connected: true }, + { id: "t_6", name: "Terminal", icon: Terminal, connected: true }, + { id: "t_7", name: "Browser", icon: Globe, connected: true }, + { id: "t_8", name: "Figma", icon: FileCode, connected: false }, + ], + currentTasks: [], + completedTasks: 8, + createdAt: "2026-03-16T14:00:00Z", + }, + { + id: "agent_3", + name: "Review Bot", + avatar: "RB", + runtimeMode: "cloud", + status: "working", + model: "Claude Sonnet 4", + description: + "Automated code reviewer. Analyzes PRs for correctness, security issues, and adherence to team coding standards.", + maxConcurrentTasks: 8, + skills: [ + { + id: "sk_6", + name: "Code Review", + description: "Review pull requests for bugs, security issues, and style violations", + }, + { + id: "sk_7", + name: "Security Audit", + description: "Check for OWASP top 10 vulnerabilities and insecure patterns", + }, + ], + tools: [ + { id: "t_9", name: "GitHub", icon: GitBranch, connected: true }, + { id: "t_10", name: "Comments", icon: MessageSquare, connected: true }, + ], + currentTasks: [ + { + id: "iss_pr47", + issueKey: "PR-47", + title: "Review: Add WebSocket reconnection logic", + status: "working", + }, + ], + completedTasks: 34, + createdAt: "2026-03-14T09:00:00Z", + }, + { + id: "agent_4", + name: "Claude-2", + avatar: "C2", + runtimeMode: "local", + status: "offline", + model: "Claude Sonnet 4", + description: + "Secondary local agent on Bohan's machine. Used for documentation and knowledge base tasks.", + maxConcurrentTasks: 1, + host: "bohan-macbook", + skills: [ + { + id: "sk_8", + name: "Documentation", + description: "Write and update technical docs, API references, and README files", + }, + ], + tools: [ + { id: "t_11", name: "GitHub", icon: GitBranch, connected: true }, + { id: "t_12", name: "Terminal", icon: Terminal, connected: true }, + ], + currentTasks: [], + completedTasks: 5, + createdAt: "2026-03-18T16:00:00Z", + }, +]; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const statusConfig: Record = { + idle: { label: "Idle", color: "text-muted-foreground", dot: "bg-muted-foreground" }, + working: { label: "Working", color: "text-green-600", dot: "bg-green-500" }, + blocked: { label: "Blocked", color: "text-yellow-600", dot: "bg-yellow-500" }, + error: { label: "Error", color: "text-red-600", dot: "bg-red-500" }, + offline: { label: "Offline", color: "text-muted-foreground/50", dot: "bg-muted-foreground/40" }, +}; + +// --------------------------------------------------------------------------- +// Components +// --------------------------------------------------------------------------- + +function AgentListItem({ + agent, + isSelected, + onClick, +}: { + agent: MockAgent; + isSelected: boolean; + onClick: () => void; +}) { + const st = statusConfig[agent.status]; + return ( -
-
-

Agents

- + + ); +} + +function SectionHeader({ + icon: Icon, + title, + count, +}: { + icon: typeof Wrench; + title: string; + count?: number; +}) { + return ( +
+ +

{title}

+ {count !== undefined && ( + ({count}) + )} +
+ ); +} + +function AgentDetail({ agent }: { agent: MockAgent }) { + const st = statusConfig[agent.status]; + + return ( +
+ {/* Header */} +
+
+ {agent.avatar} +
+
+
+

{agent.name}

+ + + {st.label} + +
+

{agent.description}

+
+
+ + {/* Meta info */} +
+
+
Runtime
+
+ {agent.runtimeMode === "cloud" ? ( + + ) : ( + + )} + {agent.runtimeMode === "cloud" ? "Cloud" : "Local"} + {agent.host && ( + ({agent.host}) + )} +
+
+
+
Model
+
{agent.model}
+
+
+
Concurrency
+
+ {agent.currentTasks.filter((t) => t.status === "working").length} / {agent.maxConcurrentTasks} slots +
+
+
+
Completed Tasks
+
{agent.completedTasks}
+
+
+ + {/* Skills */} +
+ +
+ {agent.skills.map((skill) => ( +
+
{skill.name}
+
+ {skill.description} +
+
+ ))} +
+
+ + {/* Connected Tools */} +
+ +
+ {agent.tools.map((tool) => ( +
+ + {tool.name} + {tool.connected ? ( + Connected + ) : ( + Not set up + )} +
+ ))} +
+
+ + {/* Current Tasks */} +
+ + {agent.currentTasks.length > 0 ? ( +
+ {agent.currentTasks.map((task) => ( +
+ + {task.issueKey} + + {task.title} + + {task.status === "working" ? "Working" : "Queued"} + +
+ ))} +
+ ) : ( +

No active tasks

+ )} +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Page +// --------------------------------------------------------------------------- + +export default function AgentsPage() { + const [selectedId, setSelectedId] = useState(MOCK_AGENTS[0]?.id ?? ""); + const selected = MOCK_AGENTS.find((a) => a.id === selectedId) ?? null; + + return ( +
+ {/* Left column — agent list */} +
+
+

Agents

+ +
+
+ {MOCK_AGENTS.map((agent) => ( + setSelectedId(agent.id)} + /> + ))} +
+
+ + {/* Right column — agent detail */} +
+ {selected ? ( + + ) : ( +
+ Select an agent to view details +
+ )}
-

- No agents configured yet. Add your first agent to start automating tasks. -

); } diff --git a/apps/web/app/(dashboard)/inbox/page.tsx b/apps/web/app/(dashboard)/inbox/page.tsx index 25ed704e..bfac713f 100644 --- a/apps/web/app/(dashboard)/inbox/page.tsx +++ b/apps/web/app/(dashboard)/inbox/page.tsx @@ -1,10 +1,291 @@ -export default function InboxPage() { +"use client"; + +import { useState } from "react"; +import { + AlertCircle, + Bot, + CheckCircle2, + CircleDot, + GitPullRequest, + MessageSquare, + ArrowRightLeft, +} from "lucide-react"; +import type { InboxItem, InboxItemType, InboxSeverity } from "@multica/types"; + +// --------------------------------------------------------------------------- +// Mock data +// --------------------------------------------------------------------------- + +const MOCK_INBOX_ITEMS: InboxItem[] = [ + { + id: "inb_1", + workspace_id: "ws_1", + recipient_type: "member", + recipient_id: "usr_1", + type: "agent_blocked", + severity: "action_required", + issue_id: "iss_12", + title: "Agent Claude-1 is blocked on MUL-12", + body: "I need clarification on the authentication flow. The current OAuth implementation uses PKCE, but the design doc references a session-based approach. Which one should I follow?\n\nSpecifically:\n1. Should we keep the PKCE flow for the SPA?\n2. Is the session cookie approach only for the server-rendered pages?\n3. Should I implement both and let the client decide?\n\nBlocked on this decision before I can continue with the login page implementation.", + read: false, + archived: false, + created_at: "2026-03-21T05:32:00Z", + }, + { + id: "inb_2", + workspace_id: "ws_1", + recipient_type: "member", + recipient_id: "usr_1", + type: "review_requested", + severity: "action_required", + issue_id: "iss_8", + title: "PR #47: Add WebSocket reconnection logic", + body: "Agent Codex-1 has submitted a pull request for review.\n\n**Changes:**\n- Added exponential backoff for WebSocket reconnection\n- Max retry attempts configurable via env var\n- Added connection state to the store\n- Unit tests for reconnection logic\n\n**Files changed:** 6 files (+284, -12)\n\nThe agent notes that it chose exponential backoff over linear retry because of the bursty reconnection pattern observed in the daemon logs.", + read: false, + archived: false, + created_at: "2026-03-21T04:15:00Z", + }, + { + id: "inb_3", + workspace_id: "ws_1", + recipient_type: "member", + recipient_id: "usr_1", + type: "issue_assigned", + severity: "action_required", + issue_id: "iss_15", + title: "New issue assigned: Design the agent config UI", + body: "You've been assigned to MUL-15: Design the agent config UI.\n\nPriority: High\nCreated by: Bohan\n\nDescription:\nWe need a configuration panel where users can set up their local agents — select runtime type, set concurrency limits, and manage API keys. This should live in the Settings page for now.", + read: true, + archived: false, + created_at: "2026-03-21T02:40:00Z", + }, + { + id: "inb_4", + workspace_id: "ws_1", + recipient_type: "member", + recipient_id: "usr_1", + type: "agent_completed", + severity: "attention", + issue_id: "iss_6", + title: "Agent Claude-1 completed MUL-6: API error handling", + body: "The task has been completed and all acceptance criteria passed:\n\n✅ Standardized error response format\n✅ Added error codes enum\n✅ Middleware catches panics and returns 500\n✅ All existing tests still pass\n✅ 4 new test cases added\n\nPR #45 has been created and CI is green. Ready for your review when convenient.", + read: false, + archived: false, + created_at: "2026-03-20T22:10:00Z", + }, + { + id: "inb_5", + workspace_id: "ws_1", + recipient_type: "member", + recipient_id: "usr_1", + type: "mentioned", + severity: "attention", + issue_id: "iss_10", + title: "Yuzhen mentioned you in MUL-10", + body: "@jiayuan Can you take a look at the database schema for the knowledge base? I want to make sure the vector embeddings table is set up correctly before we start indexing.\n\nI'm thinking we should use pgvector with HNSW index for the similarity search. Thoughts?", + read: true, + archived: false, + created_at: "2026-03-20T18:30:00Z", + }, + { + id: "inb_6", + workspace_id: "ws_1", + recipient_type: "member", + recipient_id: "usr_1", + type: "status_change", + severity: "info", + issue_id: "iss_3", + title: "MUL-3 moved to Done", + body: "Issue \"Set up CI/CD pipeline\" has been moved from In Review to Done by Bohan.\n\nThe GitHub Actions workflow is now running on every push to main. Build, test, and lint checks are all configured.", + read: true, + archived: false, + created_at: "2026-03-20T15:00:00Z", + }, + { + id: "inb_7", + workspace_id: "ws_1", + recipient_type: "member", + recipient_id: "usr_1", + type: "status_change", + severity: "info", + issue_id: "iss_9", + title: "MUL-9 moved to In Progress", + body: "Agent Codex-1 has started working on \"Implement issue list API endpoint\".\n\nEstimated approach:\n1. Add sqlc queries for listing/filtering issues\n2. Implement Chi handler with pagination\n3. Add sorting by priority, status, created_at\n4. Write integration tests", + read: true, + archived: false, + created_at: "2026-03-20T12:45:00Z", + }, +]; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const severityOrder: Record = { + action_required: 0, + attention: 1, + info: 2, +}; + +const typeIcons: Record = { + agent_blocked: AlertCircle, + review_requested: GitPullRequest, + issue_assigned: CircleDot, + agent_completed: CheckCircle2, + mentioned: MessageSquare, + status_change: ArrowRightLeft, +}; + +const severityColors: Record = { + action_required: "text-red-500", + attention: "text-yellow-500", + info: "text-muted-foreground", +}; + +function timeAgo(dateStr: string): string { + const diff = Date.now() - new Date(dateStr).getTime(); + const minutes = Math.floor(diff / 60000); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +// --------------------------------------------------------------------------- +// Components +// --------------------------------------------------------------------------- + +function InboxListItem({ + item, + isSelected, + onClick, +}: { + item: InboxItem; + isSelected: boolean; + onClick: () => void; +}) { + const Icon = typeIcons[item.type]; + const colorClass = severityColors[item.severity]; + + return ( + + ); +} + +function InboxDetail({ item }: { item: InboxItem }) { + const Icon = typeIcons[item.type]; + const colorClass = severityColors[item.severity]; + + const severityLabel: Record = { + action_required: "Action required", + attention: "Needs attention", + info: "Info", + }; + return (
-

Inbox

-

- Your notifications and action items will appear here. -

+ {/* Header */} +
+ +
+

{item.title}

+
+ {severityLabel[item.severity]} + · + {timeAgo(item.created_at)} + {item.issue_id && ( + <> + · + {item.issue_id} + + )} +
+
+
+ + {/* Body */} + {item.body && ( +
+ {item.body} +
+ )} +
+ ); +} + +// --------------------------------------------------------------------------- +// Page +// --------------------------------------------------------------------------- + +export default function InboxPage() { + const sorted = [...MOCK_INBOX_ITEMS].sort( + (a, b) => + severityOrder[a.severity] - severityOrder[b.severity] || + new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ); + + const [selectedId, setSelectedId] = useState(sorted[0]?.id ?? ""); + const selected = sorted.find((i) => i.id === selectedId) ?? null; + + return ( +
+ {/* Left column — inbox list */} +
+
+

Inbox

+ + {sorted.filter((i) => !i.read).length} + +
+
+ {sorted.map((item) => ( + setSelectedId(item.id)} + /> + ))} +
+
+ + {/* Right column — detail */} +
+ {selected ? ( + + ) : ( +
+ Select an item to view details +
+ )} +
); } diff --git a/apps/web/app/(dashboard)/issues/[id]/page.tsx b/apps/web/app/(dashboard)/issues/[id]/page.tsx index b6eb92f9..9be02a75 100644 --- a/apps/web/app/(dashboard)/issues/[id]/page.tsx +++ b/apps/web/app/(dashboard)/issues/[id]/page.tsx @@ -1,12 +1,297 @@ +"use client"; + +import { use } from "react"; +import Link from "next/link"; +import { + Bot, + Calendar, + ChevronRight, + User, + MessageSquare, +} from "lucide-react"; +import { + MOCK_ISSUES, + STATUS_CONFIG, + PRIORITY_CONFIG, +} from "../_data/mock"; +import type { MockAssignee } from "../_data/mock"; +import { StatusIcon, PriorityIcon } from "../page"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function timeAgo(dateStr: string): string { + const diff = Date.now() - new Date(dateStr).getTime(); + const minutes = Math.floor(diff / 60000); + if (minutes < 1) return "just now"; + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +function formatDate(date: string | null): string { + if (!date) return "—"; + return new Date(date).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); +} + +function shortDate(date: string): string { + return new Date(date).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); +} + +// --------------------------------------------------------------------------- +// Avatar +// --------------------------------------------------------------------------- + +function Avatar({ + person, + size = 20, +}: { + person: MockAssignee; + size?: number; +}) { + const isAgent = person.type === "agent"; + return ( +
+ {isAgent ? : person.avatar.charAt(0)} +
+ ); +} + +// --------------------------------------------------------------------------- +// Property row (Linear-style: label left, clickable value right) +// --------------------------------------------------------------------------- + +function PropRow({ + label, + children, +}: { + label: string; + children: React.ReactNode; +}) { + return ( +
+ {label} +
+ {children} +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Page +// --------------------------------------------------------------------------- + export default function IssueDetailPage({ params, }: { params: Promise<{ id: string }>; }) { + const { id } = use(params); + const issue = MOCK_ISSUES.find((i) => i.id === id); + + if (!issue) { + return ( +
+ Issue not found +
+ ); + } + + const statusCfg = STATUS_CONFIG[issue.status]; + const priorityCfg = PRIORITY_CONFIG[issue.priority]; + const isOverdue = + issue.dueDate && new Date(issue.dueDate) < new Date() && issue.status !== "done"; + + // Merge activity + comments into timeline + const timeline = [ + ...issue.activity.map((a) => ({ + id: a.id, + kind: "activity" as const, + actor: a.actor, + content: a.action, + createdAt: a.createdAt, + })), + ...issue.comments.map((c) => ({ + id: c.id, + kind: "comment" as const, + actor: c.author, + content: c.body, + createdAt: c.createdAt, + })), + ].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); + return ( -
-

Issue Detail

-

Issue detail view

+
+ {/* ================================================================ + LEFT: Content area + ================================================================ */} +
+ {/* Header bar */} +
+ + Issues + + + {issue.key} +
+ + {/* Content */} +
+ {/* Issue key */} +
{issue.key}
+ + {/* Title */} +

+ {issue.title} +

+ + {/* Description */} + {issue.description && ( +
+ {issue.description} +
+ )} + + {/* Separator */} +
+ + {/* Activity */} +
+

Activity

+ +
+ {timeline.map((entry, idx) => + entry.kind === "comment" ? ( + /* ---- Comment ---- */ +
+
+ + + {entry.actor.name} + + + {timeAgo(entry.createdAt)} + +
+
+ {entry.content} +
+
+ ) : ( + /* ---- Status change ---- */ +
+ + + + + + {entry.actor.name} + {" "} + {entry.content} + + {timeAgo(entry.createdAt)} +
+ ) + )} +
+ + {/* Comment input */} +
+
+ + + + + Leave a comment... + +
+
+
+
+
+ + {/* ================================================================ + RIGHT: Properties sidebar + ================================================================ */} +
+
+
+ Properties +
+ +
+ + + {statusCfg.label} + + + + + {priorityCfg.label} + + + + {issue.assignee ? ( + <> + + {issue.assignee.name} + + ) : ( + Unassigned + )} + + + + {issue.dueDate ? ( + + {shortDate(issue.dueDate)} + + ) : ( + None + )} + + + + + {issue.creator.name} + +
+ +
+ + {shortDate(issue.createdAt)} + + + {shortDate(issue.updatedAt)} + +
+
+
); } diff --git a/apps/web/app/(dashboard)/issues/_data/mock.ts b/apps/web/app/(dashboard)/issues/_data/mock.ts new file mode 100644 index 00000000..d746aee6 --- /dev/null +++ b/apps/web/app/(dashboard)/issues/_data/mock.ts @@ -0,0 +1,436 @@ +import type { IssueStatus, IssuePriority } from "@multica/types"; + +// --------------------------------------------------------------------------- +// Extended types for mock UI +// --------------------------------------------------------------------------- + +export interface MockAssignee { + id: string; + name: string; + avatar: string; + type: "member" | "agent"; +} + +export interface MockComment { + id: string; + author: MockAssignee; + body: string; + createdAt: string; +} + +export interface MockActivity { + id: string; + actor: MockAssignee; + action: string; + createdAt: string; +} + +export interface MockIssue { + id: string; + key: string; + title: string; + description: string | null; + status: IssueStatus; + priority: IssuePriority; + assignee: MockAssignee | null; + creator: MockAssignee; + dueDate: string | null; + comments: MockComment[]; + activity: MockActivity[]; + createdAt: string; + updatedAt: string; +} + +// --------------------------------------------------------------------------- +// People & Agents +// --------------------------------------------------------------------------- + +export const PEOPLE: Record = { + jiayuan: { id: "usr_1", name: "Jiayuan", avatar: "JY", type: "member" }, + bohan: { id: "usr_2", name: "Bohan", avatar: "BH", type: "member" }, + yuzhen: { id: "usr_3", name: "Yuzhen", avatar: "YZ", type: "member" }, + claude1: { id: "agent_1", name: "Claude-1", avatar: "C1", type: "agent" }, + codex1: { id: "agent_2", name: "Codex-1", avatar: "CX", type: "agent" }, + reviewBot: { id: "agent_3", name: "Review Bot", avatar: "RB", type: "agent" }, +}; + +// --------------------------------------------------------------------------- +// Status & Priority config +// --------------------------------------------------------------------------- + +export const STATUS_ORDER: IssueStatus[] = [ + "backlog", + "todo", + "in_progress", + "in_review", + "done", + "cancelled", +]; + +export const STATUS_CONFIG: Record< + IssueStatus, + { label: string; iconColor: string } +> = { + backlog: { label: "Backlog", iconColor: "text-muted-foreground" }, + todo: { label: "Todo", iconColor: "text-muted-foreground" }, + in_progress: { label: "In Progress", iconColor: "text-yellow-500" }, + in_review: { label: "In Review", iconColor: "text-blue-500" }, + done: { label: "Done", iconColor: "text-green-500" }, + blocked: { label: "Blocked", iconColor: "text-red-500" }, + cancelled: { label: "Cancelled", iconColor: "text-muted-foreground/50" }, +}; + +export const PRIORITY_CONFIG: Record< + IssuePriority, + { label: string; bars: number; color: string } +> = { + urgent: { label: "Urgent", bars: 4, color: "text-orange-500" }, + high: { label: "High", bars: 3, color: "text-orange-400" }, + medium: { label: "Medium", bars: 2, color: "text-yellow-500" }, + low: { label: "Low", bars: 1, color: "text-blue-400" }, + none: { label: "No priority", bars: 0, color: "text-muted-foreground" }, +}; + +// --------------------------------------------------------------------------- +// Mock Issues +// --------------------------------------------------------------------------- + +const { jiayuan, bohan, yuzhen, claude1, codex1, reviewBot } = PEOPLE; + +export const MOCK_ISSUES: MockIssue[] = [ + // ---- Backlog ---- + { + id: "iss_20", + key: "MUL-20", + title: "Add multi-workspace support", + description: + "Allow users to create and switch between multiple workspaces. Each workspace should have isolated issues, agents, and knowledge base.", + status: "backlog", + priority: "low", + assignee: null, + creator: jiayuan, + dueDate: null, + comments: [], + activity: [ + { id: "act_20_1", actor: jiayuan, action: "created this issue", createdAt: "2026-03-18T10:00:00Z" }, + ], + createdAt: "2026-03-18T10:00:00Z", + updatedAt: "2026-03-18T10:00:00Z", + }, + { + id: "iss_21", + key: "MUL-21", + title: "Agent long-term memory persistence", + description: + "Implement a memory system for agents that persists across task executions. Should support both vector embeddings and structured key-value storage.", + status: "backlog", + priority: "medium", + assignee: null, + creator: bohan, + dueDate: null, + comments: [], + activity: [ + { id: "act_21_1", actor: bohan, action: "created this issue", createdAt: "2026-03-19T08:00:00Z" }, + ], + createdAt: "2026-03-19T08:00:00Z", + updatedAt: "2026-03-19T08:00:00Z", + }, + + // ---- Todo ---- + { + id: "iss_15", + key: "MUL-15", + title: "Design the agent config UI", + description: + "We need a configuration panel where users can set up their local agents — select runtime type, set concurrency limits, and manage API keys. This should live in the Settings page for now.", + status: "todo", + priority: "high", + assignee: jiayuan, + creator: bohan, + dueDate: "2026-03-25T00:00:00Z", + comments: [ + { + id: "cmt_15_1", + author: bohan, + body: "Let's keep this simple for MVP — just runtime selection and concurrency slider.", + createdAt: "2026-03-20T09:00:00Z", + }, + ], + activity: [ + { id: "act_15_1", actor: bohan, action: "created this issue", createdAt: "2026-03-20T08:00:00Z" }, + { id: "act_15_2", actor: bohan, action: "assigned this to Jiayuan", createdAt: "2026-03-20T08:00:00Z" }, + { id: "act_15_3", actor: bohan, action: "set priority to High", createdAt: "2026-03-20T08:01:00Z" }, + ], + createdAt: "2026-03-20T08:00:00Z", + updatedAt: "2026-03-20T09:00:00Z", + }, + { + id: "iss_16", + key: "MUL-16", + title: "Implement knowledge base document editor", + description: + "Build a Markdown editor for creating and editing knowledge base documents. Should support basic formatting, code blocks, and image uploads.", + status: "todo", + priority: "medium", + assignee: codex1, + creator: yuzhen, + dueDate: "2026-03-28T00:00:00Z", + comments: [], + activity: [ + { id: "act_16_1", actor: yuzhen, action: "created this issue", createdAt: "2026-03-19T14:00:00Z" }, + { id: "act_16_2", actor: yuzhen, action: "assigned this to Codex-1", createdAt: "2026-03-19T14:01:00Z" }, + ], + createdAt: "2026-03-19T14:00:00Z", + updatedAt: "2026-03-19T14:01:00Z", + }, + { + id: "iss_17", + key: "MUL-17", + title: "Add issue dependency tracking", + description: "Support blocking/blocked-by relationships between issues. Show dependency graph in issue detail view.", + status: "todo", + priority: "low", + assignee: null, + creator: jiayuan, + dueDate: null, + comments: [], + activity: [ + { id: "act_17_1", actor: jiayuan, action: "created this issue", createdAt: "2026-03-20T11:00:00Z" }, + ], + createdAt: "2026-03-20T11:00:00Z", + updatedAt: "2026-03-20T11:00:00Z", + }, + + // ---- In Progress ---- + { + id: "iss_9", + key: "MUL-9", + title: "Implement issue list API endpoint", + description: + "Build the REST API endpoint for listing and filtering issues.\n\n## Requirements\n- Pagination with cursor-based approach\n- Filter by status, priority, assignee\n- Sort by priority, status, created_at\n- Include assignee info in response", + status: "in_progress", + priority: "high", + assignee: claude1, + creator: jiayuan, + dueDate: "2026-03-22T00:00:00Z", + comments: [ + { + id: "cmt_9_1", + author: claude1, + body: "Started working on this. Using sqlc for the query generation. I'll implement cursor-based pagination with the `created_at` + `id` compound cursor.", + createdAt: "2026-03-20T14:00:00Z", + }, + { + id: "cmt_9_2", + author: jiayuan, + body: "Sounds good. Make sure to add an index on (status, created_at) for the filtered queries.", + createdAt: "2026-03-20T14:30:00Z", + }, + ], + activity: [ + { id: "act_9_1", actor: jiayuan, action: "created this issue", createdAt: "2026-03-19T10:00:00Z" }, + { id: "act_9_2", actor: jiayuan, action: "assigned this to Claude-1", createdAt: "2026-03-19T10:00:00Z" }, + { id: "act_9_3", actor: claude1, action: "moved this to In Progress", createdAt: "2026-03-20T13:00:00Z" }, + ], + createdAt: "2026-03-19T10:00:00Z", + updatedAt: "2026-03-20T14:30:00Z", + }, + { + id: "iss_12", + key: "MUL-12", + title: "Implement OAuth login flow", + description: + "Set up Google OAuth for user authentication. Include PKCE flow for the SPA and session management on the server side.", + status: "in_progress", + priority: "urgent", + assignee: claude1, + creator: jiayuan, + dueDate: "2026-03-21T00:00:00Z", + comments: [ + { + id: "cmt_12_1", + author: claude1, + body: "I need clarification on the authentication flow. The current OAuth implementation uses PKCE, but the design doc references a session-based approach. Which one should I follow?", + createdAt: "2026-03-21T05:32:00Z", + }, + ], + activity: [ + { id: "act_12_1", actor: jiayuan, action: "created this issue", createdAt: "2026-03-18T09:00:00Z" }, + { id: "act_12_2", actor: jiayuan, action: "assigned this to Claude-1", createdAt: "2026-03-18T09:01:00Z" }, + { id: "act_12_3", actor: claude1, action: "moved this to In Progress", createdAt: "2026-03-20T10:00:00Z" }, + { id: "act_12_4", actor: claude1, action: "marked as Blocked", createdAt: "2026-03-21T05:32:00Z" }, + ], + createdAt: "2026-03-18T09:00:00Z", + updatedAt: "2026-03-21T05:32:00Z", + }, + { + id: "iss_10", + key: "MUL-10", + title: "Set up pgvector for knowledge base embeddings", + description: + "Configure pgvector extension and create the embeddings table for semantic search in the knowledge base.", + status: "in_progress", + priority: "medium", + assignee: yuzhen, + creator: yuzhen, + dueDate: "2026-03-24T00:00:00Z", + comments: [ + { + id: "cmt_10_1", + author: yuzhen, + body: "@jiayuan Can you take a look at the database schema? I want to make sure the vector embeddings table is set up correctly before we start indexing.", + createdAt: "2026-03-20T18:30:00Z", + }, + ], + activity: [ + { id: "act_10_1", actor: yuzhen, action: "created this issue", createdAt: "2026-03-19T11:00:00Z" }, + { id: "act_10_2", actor: yuzhen, action: "moved this to In Progress", createdAt: "2026-03-20T09:00:00Z" }, + ], + createdAt: "2026-03-19T11:00:00Z", + updatedAt: "2026-03-20T18:30:00Z", + }, + { + id: "iss_14", + key: "MUL-14", + title: "Add WebSocket event types for agent status", + description: "Define and implement WebSocket message types for real-time agent status updates (idle, working, blocked, error, offline).", + status: "in_progress", + priority: "high", + assignee: bohan, + creator: bohan, + dueDate: "2026-03-23T00:00:00Z", + comments: [], + activity: [ + { id: "act_14_1", actor: bohan, action: "created this issue", createdAt: "2026-03-20T08:00:00Z" }, + { id: "act_14_2", actor: bohan, action: "moved this to In Progress", createdAt: "2026-03-20T16:00:00Z" }, + ], + createdAt: "2026-03-20T08:00:00Z", + updatedAt: "2026-03-20T16:00:00Z", + }, + + // ---- In Review ---- + { + id: "iss_8", + key: "MUL-8", + title: "Add WebSocket reconnection logic", + description: + "Implement exponential backoff for WebSocket reconnection in the daemon. Include configurable max retry attempts.", + status: "in_review", + priority: "high", + assignee: codex1, + creator: bohan, + dueDate: "2026-03-21T00:00:00Z", + comments: [ + { + id: "cmt_8_1", + author: codex1, + body: "PR #47 submitted. Chose exponential backoff over linear retry because of the bursty reconnection pattern observed in daemon logs.", + createdAt: "2026-03-21T04:00:00Z", + }, + { + id: "cmt_8_2", + author: reviewBot, + body: "Code review passed. No security issues found. Minor suggestion: consider adding jitter to the backoff to avoid thundering herd.", + createdAt: "2026-03-21T04:30:00Z", + }, + ], + activity: [ + { id: "act_8_1", actor: bohan, action: "created this issue", createdAt: "2026-03-17T10:00:00Z" }, + { id: "act_8_2", actor: bohan, action: "assigned this to Codex-1", createdAt: "2026-03-17T10:01:00Z" }, + { id: "act_8_3", actor: codex1, action: "moved this to In Progress", createdAt: "2026-03-19T08:00:00Z" }, + { id: "act_8_4", actor: codex1, action: "moved this to In Review", createdAt: "2026-03-21T04:00:00Z" }, + ], + createdAt: "2026-03-17T10:00:00Z", + updatedAt: "2026-03-21T04:30:00Z", + }, + { + id: "iss_11", + key: "MUL-11", + title: "Implement inbox notification API", + description: "Build REST endpoints for inbox CRUD — list, mark read, archive. Include filtering by severity and type.", + status: "in_review", + priority: "medium", + assignee: claude1, + creator: jiayuan, + dueDate: "2026-03-22T00:00:00Z", + comments: [ + { + id: "cmt_11_1", + author: claude1, + body: "PR #48 is ready. All tests pass, including the new integration tests for batch mark-as-read.", + createdAt: "2026-03-21T02:00:00Z", + }, + ], + activity: [ + { id: "act_11_1", actor: jiayuan, action: "created this issue", createdAt: "2026-03-18T14:00:00Z" }, + { id: "act_11_2", actor: jiayuan, action: "assigned this to Claude-1", createdAt: "2026-03-18T14:00:00Z" }, + { id: "act_11_3", actor: claude1, action: "moved this to In Review", createdAt: "2026-03-21T02:00:00Z" }, + ], + createdAt: "2026-03-18T14:00:00Z", + updatedAt: "2026-03-21T02:00:00Z", + }, + + // ---- Done ---- + { + id: "iss_3", + key: "MUL-3", + title: "Set up CI/CD pipeline", + description: "Configure GitHub Actions for build, test, and lint on every push to main.", + status: "done", + priority: "high", + assignee: bohan, + creator: bohan, + dueDate: "2026-03-18T00:00:00Z", + comments: [], + activity: [ + { id: "act_3_1", actor: bohan, action: "created this issue", createdAt: "2026-03-15T09:00:00Z" }, + { id: "act_3_2", actor: bohan, action: "moved this to Done", createdAt: "2026-03-20T15:00:00Z" }, + ], + createdAt: "2026-03-15T09:00:00Z", + updatedAt: "2026-03-20T15:00:00Z", + }, + { + id: "iss_6", + key: "MUL-6", + title: "Standardize API error handling", + description: "Create a consistent error response format across all API endpoints. Add error codes enum and panic recovery middleware.", + status: "done", + priority: "medium", + assignee: claude1, + creator: jiayuan, + dueDate: "2026-03-20T00:00:00Z", + comments: [ + { + id: "cmt_6_1", + author: claude1, + body: "All acceptance criteria passed. PR #45 created and CI is green.", + createdAt: "2026-03-20T22:10:00Z", + }, + ], + activity: [ + { id: "act_6_1", actor: jiayuan, action: "created this issue", createdAt: "2026-03-16T10:00:00Z" }, + { id: "act_6_2", actor: jiayuan, action: "assigned this to Claude-1", createdAt: "2026-03-16T10:00:00Z" }, + { id: "act_6_3", actor: claude1, action: "moved this to Done", createdAt: "2026-03-20T22:10:00Z" }, + ], + createdAt: "2026-03-16T10:00:00Z", + updatedAt: "2026-03-20T22:10:00Z", + }, + { + id: "iss_1", + key: "MUL-1", + title: "Initialize monorepo structure", + description: "Set up the polyglot monorepo with Go backend, Next.js frontend, and shared TypeScript packages.", + status: "done", + priority: "urgent", + assignee: jiayuan, + creator: jiayuan, + dueDate: "2026-03-15T00:00:00Z", + comments: [], + activity: [ + { id: "act_1_1", actor: jiayuan, action: "created this issue", createdAt: "2026-03-14T08:00:00Z" }, + { id: "act_1_2", actor: jiayuan, action: "moved this to Done", createdAt: "2026-03-15T18:00:00Z" }, + ], + createdAt: "2026-03-14T08:00:00Z", + updatedAt: "2026-03-15T18:00:00Z", + }, +]; diff --git a/apps/web/app/(dashboard)/issues/page.tsx b/apps/web/app/(dashboard)/issues/page.tsx index 1d38961b..a033d6f1 100644 --- a/apps/web/app/(dashboard)/issues/page.tsx +++ b/apps/web/app/(dashboard)/issues/page.tsx @@ -1,15 +1,462 @@ -export default function IssuesPage() { +"use client"; + +import { useState, useCallback } from "react"; +import Link from "next/link"; +import { + Columns3, + List, + Plus, + Bot, + Circle, + CircleDashed, + CircleDot, + CircleCheck, + CircleX, + CircleAlert, + Eye, + Minus, + MessageSquare, +} from "lucide-react"; +import { + DndContext, + DragOverlay, + PointerSensor, + useSensor, + useSensors, + useDroppable, + closestCorners, + type DragStartEvent, + type DragEndEvent, +} from "@dnd-kit/core"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import type { IssueStatus, IssuePriority } from "@multica/types"; +import { + MOCK_ISSUES, + STATUS_CONFIG, + PRIORITY_CONFIG, + type MockIssue, + type MockAssignee, +} from "./_data/mock"; + +// --------------------------------------------------------------------------- +// Shared icon components +// --------------------------------------------------------------------------- + +const STATUS_ICONS: Record = { + backlog: CircleDashed, + todo: Circle, + in_progress: CircleDot, + in_review: Eye, + done: CircleCheck, + blocked: CircleAlert, + cancelled: CircleX, +}; + +export function StatusIcon({ + status, + className = "h-4 w-4", +}: { + status: IssueStatus; + className?: string; +}) { + const Icon = STATUS_ICONS[status]; + const cfg = STATUS_CONFIG[status]; + return ; +} + +export function PriorityIcon({ + priority, + className = "", +}: { + priority: IssuePriority; + className?: string; +}) { + const cfg = PRIORITY_CONFIG[priority]; + if (cfg.bars === 0) { + return ; + } return ( -
-
-

Issues

- -
-

- No issues yet. Create your first issue to get started. -

+ + {[0, 1, 2, 3].map((i) => ( + + ))} + + ); +} + +function AssigneeAvatar({ + assignee, + size = "sm", +}: { + assignee: MockAssignee | null; + size?: "sm" | "md"; +}) { + if (!assignee) return null; + const sizeClass = size === "sm" ? "h-5 w-5 text-[10px]" : "h-6 w-6 text-xs"; + return ( +
+ {assignee.type === "agent" ? ( + + ) : ( + assignee.avatar.charAt(0) + )} +
+ ); +} + +function formatDate(date: string): string { + return new Date(date).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); +} + +// --------------------------------------------------------------------------- +// Board View — Card (static, used in both draggable wrapper and overlay) +// --------------------------------------------------------------------------- + +function BoardCardContent({ issue }: { issue: MockIssue }) { + return ( +
+
+ + {issue.key} +
+

{issue.title}

+
+
+ + {issue.comments.length > 0 && ( + + + {issue.comments.length} + + )} +
+ {issue.dueDate && ( + + {formatDate(issue.dueDate)} + + )} +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Draggable card wrapper +// --------------------------------------------------------------------------- + +function DraggableBoardCard({ issue }: { issue: MockIssue }) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ + id: issue.id, + data: { status: issue.status }, + }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
+ { + // Prevent navigation when dragging + if (isDragging) e.preventDefault(); + }} + className="block transition-colors hover:opacity-80" + > + + +
+ ); +} + +// --------------------------------------------------------------------------- +// Droppable column +// --------------------------------------------------------------------------- + +function DroppableColumn({ + status, + issues, +}: { + status: IssueStatus; + issues: MockIssue[]; +}) { + const cfg = STATUS_CONFIG[status]; + const { setNodeRef, isOver } = useDroppable({ id: status }); + + return ( +
+
+ + {cfg.label} + {issues.length} +
+
+ {issues.map((issue) => ( + + ))} +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Board View (with DnD) +// --------------------------------------------------------------------------- + +function BoardView({ + issues, + onMoveIssue, +}: { + issues: MockIssue[]; + onMoveIssue: (issueId: string, newStatus: IssueStatus) => void; +}) { + const [activeIssue, setActiveIssue] = useState(null); + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { distance: 5 }, + }) + ); + + const visibleStatuses: IssueStatus[] = [ + "backlog", + "todo", + "in_progress", + "in_review", + "done", + ]; + + const handleDragStart = useCallback( + (event: DragStartEvent) => { + const issue = issues.find((i) => i.id === event.active.id); + if (issue) setActiveIssue(issue); + }, + [issues] + ); + + const handleDragEnd = useCallback( + (event: DragEndEvent) => { + setActiveIssue(null); + const { active, over } = event; + if (!over) return; + + const issueId = active.id as string; + // `over.id` is the column's droppable id (a status string) + // or another card's sortable id + let targetStatus: IssueStatus | undefined; + + if (visibleStatuses.includes(over.id as IssueStatus)) { + targetStatus = over.id as IssueStatus; + } else { + // Dropped on a card — find which column that card is in + const targetIssue = issues.find((i) => i.id === over.id); + if (targetIssue) targetStatus = targetIssue.status; + } + + if (targetStatus) { + const currentIssue = issues.find((i) => i.id === issueId); + if (currentIssue && currentIssue.status !== targetStatus) { + onMoveIssue(issueId, targetStatus); + } + } + }, + [issues, onMoveIssue, visibleStatuses] + ); + + return ( + +
+ {visibleStatuses.map((status) => ( + i.status === status)} + /> + ))} +
+ + + {activeIssue ? ( +
+ +
+ ) : null} +
+
+ ); +} + +// --------------------------------------------------------------------------- +// List View +// --------------------------------------------------------------------------- + +function ListRow({ issue }: { issue: MockIssue }) { + return ( + + + {issue.key} + + {issue.title} + {issue.dueDate && ( + + {formatDate(issue.dueDate)} + + )} + + + ); +} + +function ListView({ issues }: { issues: MockIssue[] }) { + const groupOrder: IssueStatus[] = [ + "in_review", + "in_progress", + "todo", + "backlog", + "done", + ]; + + return ( +
+ {groupOrder.map((status) => { + const cfg = STATUS_CONFIG[status]; + const filtered = issues.filter((i) => i.status === status); + if (filtered.length === 0) return null; + return ( +
+
+ + {cfg.label} + {filtered.length} +
+ {filtered.map((issue) => ( + + ))} +
+ ); + })} +
+ ); +} + +// --------------------------------------------------------------------------- +// Page +// --------------------------------------------------------------------------- + +type ViewMode = "board" | "list"; + +export default function IssuesPage() { + const [view, setView] = useState("board"); + const [issues, setIssues] = useState(MOCK_ISSUES); + + const handleMoveIssue = useCallback( + (issueId: string, newStatus: IssueStatus) => { + setIssues((prev) => + prev.map((issue) => + issue.id === issueId + ? { ...issue, status: newStatus, updatedAt: new Date().toISOString() } + : issue + ) + ); + }, + [] + ); + + return ( +
+ {/* Toolbar */} +
+
+

All Issues

+
+ + +
+
+ +
+ +
+ {view === "board" ? ( + + ) : ( + + )} +
); } diff --git a/apps/web/app/(dashboard)/knowledge-base/_data/mock.ts b/apps/web/app/(dashboard)/knowledge-base/_data/mock.ts new file mode 100644 index 00000000..c22f0f34 --- /dev/null +++ b/apps/web/app/(dashboard)/knowledge-base/_data/mock.ts @@ -0,0 +1,282 @@ +export interface KBDocument { + id: string; + title: string; + content: string; + createdBy: string; + updatedAt: string; + referencedBy: string[]; +} + +export const MOCK_DOCUMENTS: KBDocument[] = [ + { + id: "kb_1", + title: "Product Vision & Positioning", + content: `Multica is an AI-native task management platform — like Linear, but with AI agents as first-class citizens. + +## Target Users + +- 2–10 person technical teams / startups +- Teams that already use AI coding agents daily (Claude Code, Codex, etc.) +- Current toolchain: Linear/GitHub Issues + Claude Code/Codex + GitHub + IM + +## Core Value Proposition + +| Dimension | Existing Tools (Linear) | Multica | +|-----------|------------------------|---------| +| Task executor | Humans only | Humans + Agents | +| Context | Manual copy-paste | Auto-aggregated, agents read directly | +| Task flow | Human-driven status changes | Agents auto-flow + notify humans when decisions needed | +| Concurrency | Limited by team size | Parallel agent dispatch | + +## What We Are NOT + +- Not a general-purpose project management tool (no Gantt charts, resource planning) +- Not an AI agent framework (we orchestrate, not build agents) +- Not a chat product (Inbox is action-oriented, not conversational)`, + createdBy: "Jiayuan", + updatedAt: "2026-03-20T10:00:00Z", + referencedBy: ["MUL-1"], + }, + { + id: "kb_2", + title: "Architecture Overview", + content: `## System Architecture + +Polyglot monorepo — Go backend + TypeScript frontend. + +\`\`\` +server/ — Go backend (Chi + sqlc + gorilla/websocket) +apps/web/ — Next.js 16 frontend +packages/ — Shared TypeScript packages (ui, types, sdk, store, hooks, utils) +\`\`\` + +## Backend + +- **HTTP Framework**: Chi router +- **Database**: PostgreSQL 17 with pgvector extension +- **ORM/Query**: sqlc (type-safe SQL → Go code generation) +- **WebSocket**: gorilla/websocket for real-time agent status updates +- **Auth**: Google OAuth with JWT tokens + +## Frontend + +- **Framework**: Next.js 16 (App Router) +- **UI Components**: shadcn/ui (Radix + Tailwind CSS v4) +- **State**: Zustand +- **Styling**: Tailwind CSS v4, OKLCH color system + +## Agent Communication + +\`\`\` +Server ←→ WebSocket Gateway ←→ Daemon (local machine) + ↓ + Claude Code / Codex / etc. +\`\`\` + +The Daemon is a local process that maintains a persistent WebSocket connection to the server, receives task assignments, and delegates to the underlying AI runtime.`, + createdBy: "Jiayuan", + updatedAt: "2026-03-19T14:00:00Z", + referencedBy: ["MUL-9", "MUL-14"], + }, + { + id: "kb_3", + title: "API Error Handling Convention", + content: `## Error Response Format + +All API errors follow this structure: + +\`\`\`json +{ + "error": { + "code": "ISSUE_NOT_FOUND", + "message": "Issue with id 'iss_999' not found", + "details": {} + } +} +\`\`\` + +## Error Codes + +| Code | HTTP Status | Description | +|------|-------------|-------------| +| VALIDATION_ERROR | 400 | Request body/params validation failed | +| UNAUTHORIZED | 401 | Missing or invalid auth token | +| FORBIDDEN | 403 | Valid token but insufficient permissions | +| NOT_FOUND | 404 | Resource does not exist | +| CONFLICT | 409 | State conflict (e.g. duplicate) | +| INTERNAL_ERROR | 500 | Unhandled server error | + +## Panic Recovery + +The server uses a recovery middleware that catches panics, logs the stack trace, and returns a 500 with \`INTERNAL_ERROR\`. Never expose stack traces to clients.`, + createdBy: "Claude-1", + updatedAt: "2026-03-20T22:00:00Z", + referencedBy: ["MUL-6"], + }, + { + id: "kb_4", + title: "Agent Onboarding Guide", + content: `## How Agents Work in Multica + +When an issue is assigned to an agent, the following happens: + +1. Server pushes the task to the agent's inbox via WebSocket +2. Daemon receives the task and reads the issue context +3. Agent retrieves relevant Knowledge Base documents +4. Agent executes via the underlying runtime (Claude Code, Codex, etc.) +5. Progress is reported back through WebSocket status updates +6. On completion, agent creates a PR and moves the issue to "In Review" + +## Agent Capabilities + +- Receive and execute issues +- Create new issues (when blocked or discovering sub-tasks) +- Comment on issues (progress updates, questions) +- Change issue status +- Read Knowledge Base documents +- Create branches, commits, and pull requests + +## When Agents Get Blocked + +If an agent cannot proceed, it should: +1. Change the issue status to "Blocked" +2. Leave a comment explaining what it needs +3. Create an inbox notification for the assignee with severity "action_required" +4. Wait for human input before continuing`, + createdBy: "Bohan", + updatedAt: "2026-03-19T16:00:00Z", + referencedBy: ["MUL-12", "MUL-15"], + }, + { + id: "kb_5", + title: "Database Schema: Issues", + content: `## Issues Table + +\`\`\`sql +CREATE TABLE issues ( + id TEXT PRIMARY KEY DEFAULT gen_nanoid(), + workspace_id TEXT NOT NULL REFERENCES workspaces(id), + title TEXT NOT NULL, + description TEXT, + status TEXT NOT NULL DEFAULT 'backlog', + priority TEXT NOT NULL DEFAULT 'none', + assignee_type TEXT, -- 'member' | 'agent' + assignee_id TEXT, + creator_type TEXT NOT NULL, + creator_id TEXT NOT NULL, + parent_issue_id TEXT REFERENCES issues(id), + position INTEGER NOT NULL DEFAULT 0, + due_date TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +\`\`\` + +## Indexes + +\`\`\`sql +CREATE INDEX idx_issues_workspace_status ON issues(workspace_id, status); +CREATE INDEX idx_issues_assignee ON issues(assignee_type, assignee_id); +CREATE INDEX idx_issues_workspace_created ON issues(workspace_id, created_at); +\`\`\` + +## Status Values + +\`backlog\`, \`todo\`, \`in_progress\`, \`in_review\`, \`done\`, \`blocked\`, \`cancelled\` + +## Priority Values + +\`urgent\`, \`high\`, \`medium\`, \`low\`, \`none\``, + createdBy: "Yuzhen", + updatedAt: "2026-03-18T11:00:00Z", + referencedBy: ["MUL-9"], + }, + { + id: "kb_6", + title: "PR & Code Review Guidelines", + content: `## Branch Naming + +\`\`\` +feat/short-description +fix/short-description +refactor/short-description +\`\`\` + +## Commit Messages + +Follow conventional commits: + +\`\`\` +feat(scope): add new feature +fix(scope): fix a bug +refactor(scope): code restructure +docs: documentation only +test(scope): add or update tests +chore(scope): maintenance +\`\`\` + +## PR Requirements + +- Keep PRs small and focused (< 400 lines when possible) +- Include a description of what and why +- All CI checks must pass +- At least one human review required for agent-authored PRs +- Agent PRs are auto-labeled with \`agent-authored\` + +## Review Checklist + +- [ ] Does the code do what the issue describes? +- [ ] Are there tests for new behavior? +- [ ] No security vulnerabilities introduced +- [ ] No hardcoded secrets or credentials +- [ ] Error handling is appropriate`, + createdBy: "Bohan", + updatedAt: "2026-03-17T09:00:00Z", + referencedBy: ["MUL-3", "MUL-8"], + }, + { + id: "kb_7", + title: "WebSocket Protocol", + content: `## Connection + +\`\`\` +ws://localhost:8080/ws?token= +\`\`\` + +## Message Format + +All messages use JSON: + +\`\`\`json +{ + "type": "agent.status_update", + "payload": { ... }, + "timestamp": "2026-03-20T10:00:00Z" +} +\`\`\` + +## Event Types + +### Server → Client +- \`agent.status_update\` — Agent status changed (idle/working/blocked/error/offline) +- \`issue.updated\` — Issue fields changed +- \`inbox.new_item\` — New inbox notification +- \`task.progress\` — Agent reports execution progress + +### Client → Server +- \`agent.heartbeat\` — Daemon sends periodic heartbeat +- \`task.started\` — Agent began executing a task +- \`task.completed\` — Agent finished a task +- \`task.failed\` — Agent failed a task + +## Reconnection + +Daemon uses exponential backoff with jitter: +- Initial delay: 1s +- Max delay: 30s +- Jitter: ±25%`, + createdBy: "Bohan", + updatedAt: "2026-03-21T04:00:00Z", + referencedBy: ["MUL-8", "MUL-14"], + }, +]; diff --git a/apps/web/app/(dashboard)/knowledge-base/page.tsx b/apps/web/app/(dashboard)/knowledge-base/page.tsx new file mode 100644 index 00000000..e02b2ea7 --- /dev/null +++ b/apps/web/app/(dashboard)/knowledge-base/page.tsx @@ -0,0 +1,337 @@ +"use client"; + +import { useState } from "react"; +import { + FileText, + Plus, + Search, + Link as LinkIcon, +} from "lucide-react"; +import { MOCK_DOCUMENTS, type KBDocument } from "./_data/mock"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function timeAgo(dateStr: string): string { + const diff = Date.now() - new Date(dateStr).getTime(); + const hours = Math.floor(diff / 3600000); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +// --------------------------------------------------------------------------- +// Simple Markdown-ish renderer (handles headers, code blocks, tables, lists) +// --------------------------------------------------------------------------- + +function renderMarkdown(text: string): React.ReactNode[] { + const lines = text.split("\n"); + const elements: React.ReactNode[] = []; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + + // Code block + if (line.startsWith("```")) { + const lang = line.slice(3).trim(); + const codeLines: string[] = []; + i++; + while (i < lines.length && !lines[i].startsWith("```")) { + codeLines.push(lines[i]); + i++; + } + i++; // skip closing ``` + elements.push( +
+          {codeLines.join("\n")}
+        
+ ); + continue; + } + + // Table (simplified: detect | pipes) + if (line.includes("|") && line.trim().startsWith("|")) { + const tableRows: string[] = []; + while (i < lines.length && lines[i].includes("|") && lines[i].trim().startsWith("|")) { + tableRows.push(lines[i]); + i++; + } + // Filter out separator rows (|---|---|) + const dataRows = tableRows.filter((r) => !r.match(/^\|[\s-|]+\|$/)); + if (dataRows.length > 0) { + const parseRow = (row: string) => + row.split("|").filter((c) => c.trim() !== "").map((c) => c.trim()); + const header = parseRow(dataRows[0]); + const body = dataRows.slice(1).map(parseRow); + elements.push( +
+ + + + {header.map((h, hi) => ( + + ))} + + + + {body.map((row, ri) => ( + + {row.map((cell, ci) => ( + + ))} + + ))} + +
+ {h} +
+ {cell} +
+
+ ); + } + continue; + } + + // Heading + if (line.startsWith("## ")) { + elements.push( +

+ {line.slice(3)} +

+ ); + i++; + continue; + } + if (line.startsWith("### ")) { + elements.push( +

+ {line.slice(4)} +

+ ); + i++; + continue; + } + + // List item + if (line.match(/^- \[[ x]\] /)) { + const checked = line.includes("[x]"); + const text = line.replace(/^- \[[ x]\] /, ""); + elements.push( +
+ + {text} +
+ ); + i++; + continue; + } + if (line.startsWith("- ")) { + elements.push( +
+ + {renderInline(line.slice(2))} +
+ ); + i++; + continue; + } + // Numbered list + if (line.match(/^\d+\. /)) { + const num = line.match(/^(\d+)\. /)![1]; + const text = line.replace(/^\d+\. /, ""); + elements.push( +
+ {num}. + {text} +
+ ); + i++; + continue; + } + + // Empty line + if (line.trim() === "") { + elements.push(
); + i++; + continue; + } + + // Paragraph + elements.push( +

+ {renderInline(line)} +

+ ); + i++; + } + + return elements; +} + +function renderInline(text: string): React.ReactNode { + // Handle inline code `...` + const parts = text.split(/(`[^`]+`)/); + return parts.map((part, i) => { + if (part.startsWith("`") && part.endsWith("`")) { + return ( + + {part.slice(1, -1)} + + ); + } + return part; + }); +} + +// --------------------------------------------------------------------------- +// Components +// --------------------------------------------------------------------------- + +function DocListItem({ + doc, + isSelected, + onClick, +}: { + doc: KBDocument; + isSelected: boolean; + onClick: () => void; +}) { + return ( + + ); +} + +function DocDetail({ doc }: { doc: KBDocument }) { + return ( +
+
+ {/* Title */} +

{doc.title}

+ + {/* Meta */} +
+ By {doc.createdBy} + · + Updated {timeAgo(doc.updatedAt)} +
+ + {/* Content */} +
{renderMarkdown(doc.content)}
+ + {/* Referenced by */} + {doc.referencedBy.length > 0 && ( +
+
+ + Referenced by +
+
+ {doc.referencedBy.map((ref) => ( + + {ref} + + ))} +
+
+ )} +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Page +// --------------------------------------------------------------------------- + +export default function KnowledgeBasePage() { + const [selectedId, setSelectedId] = useState( + MOCK_DOCUMENTS[0]?.id ?? "" + ); + const [search, setSearch] = useState(""); + + const filtered = search + ? MOCK_DOCUMENTS.filter((d) => + d.title.toLowerCase().includes(search.toLowerCase()) + ) + : MOCK_DOCUMENTS; + + const selected = MOCK_DOCUMENTS.find((d) => d.id === selectedId) ?? null; + + return ( +
+ {/* Left: Document list */} +
+
+

Knowledge Base

+ +
+ + {/* Search */} +
+
+ + setSearch(e.target.value)} + className="flex-1 bg-transparent text-[13px] outline-none placeholder:text-muted-foreground" + /> +
+
+ + {/* Document list */} +
+ {filtered.map((doc) => ( + setSelectedId(doc.id)} + /> + ))} + {filtered.length === 0 && ( +
+ No documents found +
+ )} +
+
+ + {/* Right: Document content */} + {selected ? ( + + ) : ( +
+ Select a document +
+ )} +
+ ); +} diff --git a/apps/web/app/(dashboard)/layout.tsx b/apps/web/app/(dashboard)/layout.tsx index 4870426d..e20cf58b 100644 --- a/apps/web/app/(dashboard)/layout.tsx +++ b/apps/web/app/(dashboard)/layout.tsx @@ -6,16 +6,16 @@ import { Inbox, ListTodo, Bot, - Columns3, - Settings, + BookOpen, + ChevronDown, } from "lucide-react"; +import { MulticaIcon } from "@multica/ui/components/multica-icon"; const navItems = [ { href: "/inbox", label: "Inbox", icon: Inbox }, - { href: "/issues", label: "Issues", icon: ListTodo }, - { href: "/board", label: "Board", icon: Columns3 }, { href: "/agents", label: "Agents", icon: Bot }, - { href: "/settings", label: "Settings", icon: Settings }, + { href: "/issues", label: "Issues", icon: ListTodo }, + { href: "/knowledge-base", label: "Knowledge Base", icon: BookOpen }, ]; export default function DashboardLayout({ @@ -26,26 +26,34 @@ export default function DashboardLayout({ const pathname = usePathname(); return ( -
- {/* Sidebar */} -