diff --git a/.env.example b/.env.example index 312ec87a..1c0c93ab 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,10 @@ MULTICA_CODEX_MODEL= MULTICA_CODEX_WORKDIR= MULTICA_CODEX_TIMEOUT=20m +# Email (Resend) +RESEND_API_KEY= +RESEND_FROM_EMAIL=noreply@multica.ai + # Google OAuth GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= diff --git a/.gitignore b/.gitignore index d9c40de0..7d7617b7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules dist *.log .DS_Store +.envrc # build outputs .next diff --git a/CLAUDE.md b/CLAUDE.md index b6d94bfb..77ef183f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,6 +39,8 @@ apps/web/ | `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. @@ -91,6 +93,7 @@ Browser ← WSClient (shared/api) ← WebSocket ← Hub.Broadcast() ← Handlers - **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. - **Database**: 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). @@ -142,7 +145,8 @@ make db-down # Stop shared PostgreSQL ### Worktree Support -For isolated feature testing with a separate database: +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 diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx index b1c2cee2..4fab6b0c 100644 --- a/apps/web/app/(auth)/login/page.tsx +++ b/apps/web/app/(auth)/login/page.tsx @@ -4,7 +4,6 @@ import { Suspense, useState } from "react"; import { useSearchParams, useRouter } from "next/navigation"; import { useAuthStore } from "@/features/auth"; import { useWorkspaceStore } from "@/features/workspace"; -import { useNavigationStore } from "@/features/navigation"; import { api } from "@/shared/api"; import { Card, @@ -41,9 +40,8 @@ function LoginPageContent() { await login(email, name || undefined); const wsList = await api.listWorkspaces(); await hydrateWorkspace(wsList); - const fallback = useNavigationStore.getState().lastPath; - router.push(searchParams.get("next") || fallback); - } catch (err) { + router.push(searchParams.get("next") || "/issues"); + } catch { setError("Login failed. Make sure the server is running."); setSubmitting(false); } diff --git a/apps/web/app/(dashboard)/issues/[id]/page.test.tsx b/apps/web/app/(dashboard)/issues/[id]/page.test.tsx index 4840cfc2..05fd853a 100644 --- a/apps/web/app/(dashboard)/issues/[id]/page.test.tsx +++ b/apps/web/app/(dashboard)/issues/[id]/page.test.tsx @@ -1,6 +1,6 @@ -import { Suspense, forwardRef, useState, useImperativeHandle } from "react"; +import { Suspense, forwardRef, useRef, useState, useImperativeHandle } from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen, waitFor, act } from "@testing-library/react"; +import { render, screen, waitFor, act, fireEvent } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import type { Issue, Comment } from "@/shared/types"; @@ -74,16 +74,18 @@ vi.mock("@/components/ui/calendar", () => ({ // Mock RichTextEditor (Tiptap needs real DOM) vi.mock("@/components/common/rich-text-editor", () => ({ RichTextEditor: forwardRef(({ defaultValue, onUpdate, placeholder, onSubmit }: any, ref: any) => { + const valueRef = useRef(defaultValue || ""); const [value, setValue] = useState(defaultValue || ""); useImperativeHandle(ref, () => ({ - getMarkdown: () => value, - clearContent: () => setValue(""), + getMarkdown: () => valueRef.current, + clearContent: () => { valueRef.current = ""; setValue(""); }, focus: () => {}, })); return (