diff --git a/apps/web/app/favicon.ico/route.ts b/apps/web/app/favicon.ico/route.ts new file mode 100644 index 00000000..00890acb --- /dev/null +++ b/apps/web/app/favicon.ico/route.ts @@ -0,0 +1,3 @@ +export function GET(request: Request) { + return Response.redirect(new URL("/favicon.svg", request.url), 308); +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 5a544793..f83a84b2 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -8,6 +8,10 @@ import "./globals.css"; export const metadata: Metadata = { title: "Multica", description: "AI-native task management", + icons: { + icon: [{ url: "/favicon.svg", type: "image/svg+xml" }], + shortcut: ["/favicon.svg"], + }, }; export default function RootLayout({ diff --git a/apps/web/app/pair/local/page.test.tsx b/apps/web/app/pair/local/page.test.tsx new file mode 100644 index 00000000..f4096865 --- /dev/null +++ b/apps/web/app/pair/local/page.test.tsx @@ -0,0 +1,101 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { render, screen, waitFor } from "@testing-library/react"; + +const { + mockGetDaemonPairingSession, + mockApproveDaemonPairingSession, + mockWorkspace, + mockAuthValue, +} = vi.hoisted(() => ({ + mockGetDaemonPairingSession: vi.fn(), + mockApproveDaemonPairingSession: vi.fn(), + mockWorkspace: { + id: "05ce77f1-7c45-4735-b1f7-619347f7f76c", + name: "Jiayuan's Workspace", + slug: "jiayuan-05ce77f1", + description: null, + settings: {}, + created_at: "2026-03-24T00:00:00Z", + updated_at: "2026-03-24T00:00:00Z", + }, + mockAuthValue: { + user: { + id: "user-1", + name: "Jiayuan", + email: "jiayuan@example.com", + avatar_url: null, + created_at: "2026-03-24T00:00:00Z", + updated_at: "2026-03-24T00:00:00Z", + }, + workspaces: [] as Array<{ + id: string; + name: string; + slug: string; + description: null; + settings: Record; + created_at: string; + updated_at: string; + }>, + workspace: null as null | { + id: string; + name: string; + slug: string; + description: null; + settings: Record; + created_at: string; + updated_at: string; + }, + isLoading: false, + }, +})); + +mockAuthValue.workspaces = [mockWorkspace]; +mockAuthValue.workspace = mockWorkspace; + +vi.mock("next/navigation", () => ({ + useSearchParams: () => new URLSearchParams("token=test-token"), +})); + +vi.mock("../../../lib/api", () => ({ + api: { + getDaemonPairingSession: mockGetDaemonPairingSession, + approveDaemonPairingSession: mockApproveDaemonPairingSession, + }, +})); + +vi.mock("../../../lib/auth-context", () => ({ + useAuth: () => mockAuthValue, +})); + +import LocalDaemonPairPage from "./page"; + +describe("LocalDaemonPairPage", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockGetDaemonPairingSession.mockResolvedValue({ + token: "test-token", + daemon_id: "local-daemon", + device_name: "Jiayuans-MacBook-Pro.local", + runtime_name: "Local Codex", + runtime_type: "codex", + runtime_version: "codex-cli 0.116.0", + workspace_id: mockWorkspace.id, + status: "pending", + approved_at: null, + claimed_at: null, + expires_at: "2026-03-24T07:20:00Z", + link_url: null, + }); + }); + + it("shows the selected workspace name instead of the raw id", async () => { + render(); + + await waitFor(() => { + expect(mockGetDaemonPairingSession).toHaveBeenCalledWith("test-token"); + }); + + expect(await screen.findByText("Jiayuan's Workspace")).toBeInTheDocument(); + expect(screen.queryByText(mockWorkspace.id)).not.toBeInTheDocument(); + }); +}); diff --git a/apps/web/app/pair/local/page.tsx b/apps/web/app/pair/local/page.tsx index 8dc70471..3d2c3b66 100644 --- a/apps/web/app/pair/local/page.tsx +++ b/apps/web/app/pair/local/page.tsx @@ -9,7 +9,6 @@ import { Label } from "@multica/ui/components/ui/label"; import { Select, SelectTrigger, - SelectValue, SelectContent, SelectItem, } from "@multica/ui/components/ui/select"; @@ -39,6 +38,10 @@ function LocalDaemonPairPageContent() { const next = `/pair/local?token=${encodeURIComponent(token)}`; return `/login?next=${encodeURIComponent(next)}`; }, [token]); + const selectedWorkspace = useMemo( + () => workspaces.find((item) => item.id === selectedWorkspaceId) ?? null, + [selectedWorkspaceId, workspaces], + ); useEffect(() => { if (!token) { @@ -135,7 +138,9 @@ function LocalDaemonPairPageContent() {