From ab48eafe615928adbb69602a1a5165bed747d9d7 Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Wed, 1 Apr 2026 15:49:01 +0800 Subject: [PATCH 1/3] feat(agents): add avatar upload to agent settings page Add avatar upload UI to the agent SettingsTab, matching the existing member avatar upload pattern. Also update the agent list item and detail header to display the uploaded avatar image. --- apps/web/app/(dashboard)/agents/page.tsx | 77 ++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/apps/web/app/(dashboard)/agents/page.tsx b/apps/web/app/(dashboard)/agents/page.tsx index a6e8c092..e87934a8 100644 --- a/apps/web/app/(dashboard)/agents/page.tsx +++ b/apps/web/app/(dashboard)/agents/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { useDefaultLayout } from "react-resizable-panels"; import { Bot, @@ -28,6 +28,7 @@ import { Globe, Lock, Settings, + Camera, } from "lucide-react"; import type { Agent, @@ -74,6 +75,7 @@ import { useAuthStore } from "@/features/auth"; import { useWorkspaceStore } from "@/features/workspace"; import { useRuntimeStore } from "@/features/runtimes"; import { useIssueStore } from "@/features/issues"; +import { useFileUpload } from "@/shared/hooks/use-file-upload"; // --------------------------------------------------------------------------- @@ -341,8 +343,12 @@ function AgentListItem({ isSelected ? "bg-accent" : "hover:bg-accent/50" }`} > -
- {getInitials(agent.name)} +
+ {agent.avatar_url ? ( + {agent.name} + ) : ( + getInitials(agent.name) + )}
@@ -1173,6 +1179,22 @@ function SettingsTab({ const [visibility, setVisibility] = useState(agent.visibility); const [maxTasks, setMaxTasks] = useState(agent.max_concurrent_tasks); const [saving, setSaving] = useState(false); + const { upload, uploading } = useFileUpload(); + const fileInputRef = useRef(null); + + const handleAvatarUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + e.target.value = ""; + try { + const result = await upload(file); + if (!result) return; + await onSave({ avatar_url: result.link }); + toast.success("Avatar updated"); + } catch (err) { + toast.error(err instanceof Error ? err.message : "Failed to upload avatar"); + } + }; const dirty = name !== agent.name || @@ -1200,6 +1222,47 @@ function SettingsTab({ return (
+
+ +
+ + +
+ Click to upload avatar +
+
+
+
{/* Header */}
-
- {getInitials(agent.name)} +
+ {agent.avatar_url ? ( + {agent.name} + ) : ( + getInitials(agent.name) + )}
From 98e7d27acca7b2373ebcf467a72fcb48a2beebfd Mon Sep 17 00:00:00 2001 From: LinYushen Date: Wed, 1 Apr 2026 15:57:23 +0800 Subject: [PATCH 2/3] feat(cli): add --attachment flag to issue comment add (#260) Add file attachment support to `multica issue comment add`. The CLI uploads files via multipart form to /api/upload-file, collects the returned attachment IDs, and passes them when creating the comment. Usage: multica issue comment add --content "..." --attachment file1.png --attachment file2.pdf Co-authored-by: Claude Opus 4.6 (1M context) --- server/cmd/multica/cmd_issue.go | 33 +++++++++++++++++-- server/internal/cli/client.go | 56 +++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/server/cmd/multica/cmd_issue.go b/server/cmd/multica/cmd_issue.go index e7783354..9ce65c1e 100644 --- a/server/cmd/multica/cmd_issue.go +++ b/server/cmd/multica/cmd_issue.go @@ -147,6 +147,7 @@ func init() { // issue comment add issueCommentAddCmd.Flags().String("content", "", "Comment content (required)") issueCommentAddCmd.Flags().String("parent", "", "Parent comment ID (reply to a specific comment)") + issueCommentAddCmd.Flags().StringSlice("attachment", nil, "File path(s) to attach (can be specified multiple times)") issueCommentAddCmd.Flags().String("output", "json", "Output format: table or json") } @@ -540,19 +541,45 @@ func runIssueCommentAdd(cmd *cobra.Command, args []string) error { return err } - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + issueID := args[0] + + // Use a longer timeout when attachments are present (file uploads can be slow). + timeout := 15 * time.Second + attachments, _ := cmd.Flags().GetStringSlice("attachment") + if len(attachments) > 0 { + timeout = 60 * time.Second + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() + // Upload attachments and collect their IDs. + var attachmentIDs []string + for _, filePath := range attachments { + data, readErr := os.ReadFile(filePath) + if readErr != nil { + return fmt.Errorf("read attachment %s: %w", filePath, readErr) + } + id, uploadErr := client.UploadFile(ctx, data, filePath, issueID) + if uploadErr != nil { + return fmt.Errorf("upload attachment %s: %w", filePath, uploadErr) + } + attachmentIDs = append(attachmentIDs, id) + fmt.Fprintf(os.Stderr, "Uploaded %s\n", filePath) + } + body := map[string]any{"content": content} if parentID, _ := cmd.Flags().GetString("parent"); parentID != "" { body["parent_id"] = parentID } + if len(attachmentIDs) > 0 { + body["attachment_ids"] = attachmentIDs + } var result map[string]any - if err := client.PostJSON(ctx, "/api/issues/"+args[0]+"/comments", body, &result); err != nil { + if err := client.PostJSON(ctx, "/api/issues/"+issueID+"/comments", body, &result); err != nil { return fmt.Errorf("add comment: %w", err) } - fmt.Fprintf(os.Stderr, "Comment added to issue %s.\n", truncateID(args[0])) + fmt.Fprintf(os.Stderr, "Comment added to issue %s.\n", truncateID(issueID)) output, _ := cmd.Flags().GetString("output") if output == "table" { diff --git a/server/internal/cli/client.go b/server/internal/cli/client.go index 312276d0..8cfce31b 100644 --- a/server/internal/cli/client.go +++ b/server/internal/cli/client.go @@ -6,7 +6,9 @@ import ( "encoding/json" "fmt" "io" + "mime/multipart" "net/http" + "path/filepath" "strings" "time" ) @@ -156,6 +158,60 @@ func (c *APIClient) PutJSON(ctx context.Context, path string, body any, out any) return json.NewDecoder(resp.Body).Decode(out) } +// UploadFile uploads a file via multipart form to /api/upload-file. +// It returns the attachment ID from the server response. +func (c *APIClient) UploadFile(ctx context.Context, fileData []byte, filename string, issueID string) (string, error) { + var body bytes.Buffer + writer := multipart.NewWriter(&body) + + part, err := writer.CreateFormFile("file", filepath.Base(filename)) + if err != nil { + return "", fmt.Errorf("create form file: %w", err) + } + if _, err := part.Write(fileData); err != nil { + return "", fmt.Errorf("write file data: %w", err) + } + + if issueID != "" { + if err := writer.WriteField("issue_id", issueID); err != nil { + return "", fmt.Errorf("write issue_id field: %w", err) + } + } + + if err := writer.Close(); err != nil { + return "", fmt.Errorf("close multipart writer: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/api/upload-file", &body) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + c.setHeaders(req) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + respData, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + return "", fmt.Errorf("upload file returned %d: %s", resp.StatusCode, strings.TrimSpace(string(respData))) + } + + var result map[string]any + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "", fmt.Errorf("decode upload response: %w", err) + } + + id, _ := result["id"].(string) + if id == "" { + return "", fmt.Errorf("upload response missing attachment id") + } + return id, nil +} + // HealthCheck hits the /health endpoint and returns the response body. func (c *APIClient) HealthCheck(ctx context.Context) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+"/health", nil) From 3e96240cec92459270fefe85fcdbc3e974c18f85 Mon Sep 17 00:00:00 2001 From: LinYushen Date: Wed, 1 Apr 2026 15:58:20 +0800 Subject: [PATCH 3/3] docs: align AGENTS.md with CLAUDE.md content (#263) AGENTS.md was a minimal 17-line summary while CLAUDE.md had comprehensive project documentation. Updated AGENTS.md to include all sections from CLAUDE.md (architecture, state management, backend structure, UI/UX rules, worktree support, E2E patterns, verification loop) while preserving AGENTS.md-unique details (naming conventions, test directory, PR guidelines). Co-authored-by: Claude Opus 4.6 (1M context) --- AGENTS.md | 278 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 268 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fd66fd32..8b224583 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,16 +1,274 @@ # Repository Guidelines -## Project Structure & Module Organization -`apps/web/` contains the Next.js 16 frontend: routes live in `app/`, reusable UI in `components/`, feature code in `features/`, test utilities in `test/`, and static assets in `public/`. `server/` contains the Go backend: entry points are in `cmd/{server,multica,migrate}`, application logic lives in `internal/`, migrations are in `migrations/`, and SQL lives under `pkg/db/queries/` with generated sqlc output in `pkg/db/generated/`. `e2e/` holds Playwright coverage. `scripts/` and the root `Makefile` drive local setup and verification. +This file provides guidance to AI agents when working with code in this repository. -## Build, Test, and Development Commands -Use `make setup` for first-time setup: it installs dependencies, ensures PostgreSQL is running, and applies migrations. Use `make start` to run backend and frontend together with `.env` or `.env.worktree`. For single-surface work, use `pnpm dev:web` for the frontend and `make dev` for the Go server. Run `pnpm test` for Vitest, `make test` for Go tests, and `make check` for the full pipeline: typecheck, frontend unit tests, Go tests, then Playwright. After changing SQL in `server/pkg/db/queries/*.sql`, run `make sqlc`. +## Project Context -## Coding Style & Naming Conventions -TypeScript in `apps/web` uses 2-space indentation, double quotes, and semicolons. Prefer PascalCase for React components, camelCase for hooks and helpers, and colocated test files such as `page.test.tsx`. Go code should stay `gofmt`-clean and use domain-oriented filenames like `issue.go` or `cmd_issue.go`. Do not hand-edit generated code in `server/pkg/db/generated/`. +Multica is an AI-native task management platform — like Linear, but with AI agents as first-class citizens. -## Testing Guidelines -Frontend unit tests use Vitest with Testing Library and shared setup from `apps/web/test/`. End-to-end tests live in `e2e/*.spec.ts`; `make check` will start missing services automatically, while direct Playwright runs expect the app to already be running. Backend tests use Go’s standard `*_test.go` pattern. Add or update tests whenever you change handlers, CLI commands, daemon behavior, or SQL-backed flows. +- Agents can be assigned issues, create issues, comment, and change status +- Supports local (daemon) and cloud agent runtimes +- Built for 2-10 person AI-native teams -## Commit & Pull Request Guidelines -Recent history follows conventional commits with scopes, for example `feat(web): ...`, `fix(cli): ...`, `refactor(daemon): ...`, `test(cli): ...`, and `docs: ...`. Keep PRs focused and include a short description, linked issue or PR number when relevant, screenshots for UI work, and notes for migrations, env changes, or CLI surface changes. Before opening a PR, run `make check` or the relevant frontend/backend subset. +## Architecture + +**Go backend + standalone Next.js frontend.** + +- `server/` — Go backend (Chi router, sqlc for DB, gorilla/websocket for real-time) +- `apps/web/` — Next.js 16 frontend (App Router) — self-contained, no shared package dependencies +- `e2e/` — Playwright end-to-end tests +- `scripts/` and root `Makefile` — local setup and verification + +### Web App Structure (`apps/web/`) + +The frontend uses a **feature-based architecture** with four layers: + +``` +apps/web/ +├── app/ # Routing layer (thin shells — import from features/) +├── features/ # Business logic, organized by domain +├── shared/ # Cross-feature utilities (api client, types, logger) +├── test/ # Shared test utilities and setup +├── public/ # Static assets +``` + +**`app/`** — Next.js App Router pages. Route files should be thin: import and re-export from `features/`. Layout components and route-specific glue (redirects, auth guards) live here. Shared layout components (e.g. `app-sidebar`) stay in `app/(dashboard)/_components/`. + +**`features/`** — Domain modules, each with its own components, hooks, stores, and config: + +| Feature | Purpose | Exports | +|---|---|---| +| `features/auth/` | Authentication state | `useAuthStore`, `AuthInitializer` | +| `features/workspace/` | Workspace, members, agents | `useWorkspaceStore`, `useActorName` | +| `features/issues/` | Issue state, components, config | `useIssueStore`, icons, pickers, status/priority config | +| `features/inbox/` | Inbox notification state | `useInboxStore` | +| `features/realtime/` | WebSocket connection + sync | `WSProvider`, `useWSEvent`, `useRealtimeSync` | +| `features/modals/` | Modal registry and state | Modal store and components | +| `features/skills/` | Skill management | Skill components | + +**`shared/`** — Code used across multiple features: +- `shared/api/` — `ApiClient` (REST) and `WSClient` (WebSocket) for backend communication, plus the `api` singleton. +- `shared/types/` — Domain types (Issue, Agent, Workspace, etc.) and WebSocket event types. +- `shared/logger.ts` — Logger utility. + +### State Management + +- **Zustand** for global client state — one store per feature domain (`features/auth/store.ts`, `features/workspace/store.ts`, `features/issues/store.ts`, `features/inbox/store.ts`). +- **React Context** only for connection lifecycle (`WSProvider` in `features/realtime/`). +- **Local `useState`** for component-scoped UI state (forms, modals, filters). +- Do not use React Context for data that can be a zustand store. + +**Store conventions:** +- One store per feature domain. Import via `useAuthStore(selector)` or `useWorkspaceStore(selector)`. +- Stores must not call `useRouter` or any React hooks — keep navigation in components. +- Cross-store reads use `useOtherStore.getState()` inside actions (not hooks). +- Dependency direction: `workspace` → `auth`, `realtime` → `auth`, `issues` → `workspace`. Never reverse. + +### Import Aliases + +Use `@/` alias (maps to `apps/web/`): +```typescript +import { api } from "@/shared/api"; +import type { Issue } from "@/shared/types"; +import { useAuthStore } from "@/features/auth"; +import { useWorkspaceStore } from "@/features/workspace"; +import { useIssueStore } from "@/features/issues"; +import { useInboxStore } from "@/features/inbox"; +import { useWSEvent } from "@/features/realtime"; +import { StatusIcon } from "@/features/issues/components"; +``` + +Within a feature, use relative imports. Between features or to shared, use `@/`. + +### Data Flow + +``` +Browser → ApiClient (shared/api) → REST API (Chi handlers) → sqlc queries → PostgreSQL +Browser ← WSClient (shared/api) ← WebSocket ← Hub.Broadcast() ← Handlers/TaskService +``` + +### Backend Structure (`server/`) + +- **Entry points** (`cmd/`): `server` (HTTP API), `multica` (CLI — daemon, agent management, config), `migrate` +- **Handlers** (`internal/handler/`): One file per domain (issue, comment, agent, auth, daemon, etc.). Each handler holds `Queries`, `DB`, `Hub`, and `TaskService`. +- **Real-time** (`internal/realtime/`): Hub manages WebSocket clients. Server broadcasts events; inbound WS message routing is still TODO. +- **Auth** (`internal/auth/` + `internal/middleware/`): JWT (HS256). Middleware sets `X-User-ID` and `X-User-Email` headers. Login creates user on-the-fly if not found. +- **Task lifecycle** (`internal/service/task.go`): Orchestrates agent work — enqueue → claim → start → complete/fail. Syncs issue status automatically and broadcasts WS events at each transition. +- **Agent SDK** (`pkg/agent/`): Unified `Backend` interface for executing prompts via Claude Code or Codex. Each backend spawns its CLI and streams results via `Session.Messages` + `Session.Result` channels. +- **Daemon** (`internal/daemon/`): Local agent runtime — auto-detects available CLIs (claude, codex), registers runtimes, polls for tasks, routes by provider. +- **CLI** (`internal/cli/`): Shared helpers for the `multica` CLI — API client, config management, output formatting. +- **Events** (`internal/events/`): Internal event bus for decoupled communication between handlers and services. +- **Logging** (`internal/logger/`): Structured logging via slog. `LOG_LEVEL` env var controls level (debug, info, warn, error). +- **Database**: PostgreSQL with pgvector extension (`pgvector/pgvector:pg17`). sqlc generates Go code from SQL in `pkg/db/queries/` → `pkg/db/generated/`. Migrations in `migrations/`. +- **Routes** (`cmd/server/router.go`): Public routes (auth, health, ws) + protected routes (require JWT) + daemon routes (unauthenticated, separate auth model). + +### Multi-tenancy + +All queries filter by `workspace_id`. Membership checks gate access. `X-Workspace-ID` header routes requests to the correct workspace. + +### Agent Assignees + +Assignees are polymorphic — can be a member or an agent. `assignee_type` + `assignee_id` on issues. Agents render with distinct styling (purple background, robot icon). + +## Commands + +```bash +# One-click setup & run +make setup # First-time: ensure shared DB, create app DB, migrate +make start # Start backend + frontend together +make stop # Stop app processes for the current checkout +make db-down # Stop the shared PostgreSQL container + +# Frontend +pnpm install +pnpm dev:web # Next.js dev server (port 3000) +pnpm build # Build frontend +pnpm typecheck # TypeScript check +pnpm lint # ESLint via Next.js +pnpm test # TS tests (Vitest) + +# Backend (Go) +make dev # Run Go server (port 8080) +make daemon # Run local daemon +make build # Build server + CLI binaries to server/bin/ +make cli ARGS="..." # Run multica CLI (e.g. make cli ARGS="config") +make test # Go tests +make sqlc # Regenerate sqlc code after editing SQL in server/pkg/db/queries/ +make migrate-up # Run database migrations +make migrate-down # Rollback migrations + +# Run a single Go test +cd server && go test ./internal/handler/ -run TestName + +# Run a single TS test +pnpm --filter @multica/web exec vitest run src/path/to/file.test.ts + +# Run a single E2E test (requires backend + frontend running) +pnpm exec playwright test e2e/tests/specific-test.spec.ts + +# Infrastructure +make db-up # Start shared PostgreSQL (pgvector/pg17 image) +make db-down # Stop shared PostgreSQL +``` + +### CI Requirements + +CI runs on Node 22 and Go 1.24 with a `pgvector/pgvector:pg17` PostgreSQL service. See `.github/workflows/ci.yml`. + +### Worktree Support + +All checkouts share one PostgreSQL container. Isolation is at the database level — each worktree gets its own DB name and unique ports via `.env.worktree`. Main checkouts use `.env`. + +```bash +make worktree-env # Generate .env.worktree with unique DB/ports +make setup-worktree # Setup using .env.worktree +make start-worktree # Start using .env.worktree +``` + +## Coding Rules + +- TypeScript strict mode is enabled; keep types explicit. +- TypeScript in `apps/web` uses 2-space indentation, double quotes, and semicolons. +- Prefer PascalCase for React components, camelCase for hooks and helpers, and colocated test files such as `page.test.tsx`. +- Go code follows standard Go conventions (gofmt, go vet). Use domain-oriented filenames like `issue.go` or `cmd_issue.go`. +- Do not hand-edit generated code in `server/pkg/db/generated/`. +- Keep comments in code **English only**. +- Prefer existing patterns/components over introducing parallel abstractions. +- Unless the user explicitly asks for backwards compatibility, do **not** add compatibility layers, fallback paths, dual-write logic, legacy adapters, or temporary shims. +- If a flow or API is being replaced and the product is not yet live, prefer removing the old path instead of preserving both old and new behavior. +- Treat compatibility code as a maintenance cost, not a default safety mechanism. Avoid "just in case" branches that make the codebase harder to reason about. +- Avoid broad refactors unless required by the task. + +## UI/UX Rules + +- Prefer shadcn components over custom implementations. Install missing components via `npx shadcn add`. +- **Feature-specific components** → `features//components/` — issue icons, pickers, and other domain-bound UI live inside their feature module. +- Use shadcn design tokens for styling (e.g. `bg-primary`, `text-muted-foreground`, `text-destructive`). Avoid hardcoded color values (e.g. `text-red-500`, `bg-gray-100`). +- Do not introduce extra state (useState, context, reducers) unless explicitly required by the design. Prefer zustand stores for shared state over React Context. +- Pay close attention to **overflow** (truncate long text, scrollable containers), **alignment**, and **spacing** consistency. +- When unsure about interaction or state design, ask — the user will provide direction. + +## Testing Rules + +- **TypeScript**: Vitest with Testing Library. Shared test setup lives in `apps/web/test/`. Mock external/third-party dependencies only. +- **Go**: Standard `go test`. Tests should create their own fixture data in a test database. +- End-to-end tests live in `e2e/*.spec.ts`; `make check` will start missing services automatically, while direct Playwright runs expect the app to already be running. +- Add or update tests whenever you change handlers, CLI commands, daemon behavior, or SQL-backed flows. + +## Commit & Pull Request Rules + +- Use atomic commits grouped by logical intent. +- Conventional format with scopes: + - `feat(web): ...`, `feat(cli): ...` + - `fix(web): ...`, `fix(cli): ...` + - `refactor(daemon): ...` + - `test(cli): ...` + - `docs: ...` + - `chore(scope): ...` +- Keep PRs focused and include a short description, linked issue or PR number when relevant, screenshots for UI work, and notes for migrations, env changes, or CLI surface changes. +- Before opening a PR, run `make check` or the relevant frontend/backend subset. + +## Minimum Pre-Push Checks + +```bash +make check # Runs all checks: typecheck, unit tests, Go tests, E2E +``` + +Run verification only when the user explicitly asks for it. + +For targeted checks when requested: +```bash +pnpm typecheck # TypeScript type errors only +pnpm test # TS unit tests only (Vitest) +make test # Go tests only +pnpm exec playwright test # E2E only (requires backend + frontend running) +``` + +## AI Agent Verification Loop + +After writing or modifying code, always run the full verification pipeline: + +```bash +make check +``` + +This runs all checks in sequence: +1. TypeScript typecheck (`pnpm typecheck`) +2. TypeScript unit tests (`pnpm test`) +3. Go tests (`go test ./...`) +4. E2E tests (auto-starts backend + frontend if needed, runs Playwright) + +**Workflow:** +- Write code to satisfy the requirement +- Run `make check` +- If any step fails, read the error output, fix the code, and re-run `make check` +- Repeat until all checks pass +- Only then consider the task complete + +**Quick iteration:** If you know only TypeScript or Go is affected, run individual checks first for faster feedback, then finish with a full `make check` before marking work complete. + +## E2E Test Patterns + +E2E tests should be self-contained. Use the `TestApiClient` fixture for data setup/teardown: + +```typescript +import { loginAsDefault, createTestApi } from "./helpers"; +import type { TestApiClient } from "./fixtures"; + +let api: TestApiClient; + +test.beforeEach(async ({ page }) => { + api = await createTestApi(); // logged-in API client + await loginAsDefault(page); // browser session +}); + +test.afterEach(async () => { + await api.cleanup(); // delete any data created during the test +}); + +test("example", async ({ page }) => { + const issue = await api.createIssue("Test Issue"); // create via API + await page.goto(`/issues/${issue.id}`); // test via UI + // api.cleanup() in afterEach removes the issue +}); +```