Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/app/invite/[token]/accept-invite-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function AcceptInviteButton({ token }: { token: string }) {
onSuccess: ({ workspaceSlug, projectSlug }) => {
router.push(
projectSlug
? `/w/${workspaceSlug}/projects/${projectSlug}/overview`
? `/w/${workspaceSlug}/projects/${projectSlug}/engineer`
: `/w/${workspaceSlug}`,
);
router.refresh();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from "@foundry/domain";
import { getCurrentUser } from "@/server/session";
import { EngineerStage, type EngineerView } from "@/components/stages/engineer-stage";
import { PipelineKickoffListener } from "@/components/pipeline-kickoff";

export default async function StagePage({
params,
Expand Down Expand Up @@ -59,6 +60,12 @@ export default async function StagePage({
(s) => s.stage === "VERIFY" && s.branchId === branchId,
);

// The workbench is the landing page now, so the create-flow kickoff prompt
// fires here instead of on the old overview page.
const brief = await prisma.projectBrief.findUnique({
where: { projectId_branchId: { projectId: project.id, branchId } },
});

const engineerViews = [
"sourcing",
"schematic",
Expand All @@ -79,6 +86,7 @@ export default async function StagePage({

return (
<div className="h-full">
<PipelineKickoffListener hasBrief={Boolean(brief?.prompt || brief?.intendedUse)} />
<Suspense fallback={<div className="bg-muted/30 h-full" />}>
<EngineerStage
projectId={project.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default async function ProjectShellLayout({
}) {
const { workspaceSlug, projectSlug } = await params;
const user = await getCurrentUser();
if (!user) redirect(`/auth/sign-in?next=/w/${workspaceSlug}/projects/${projectSlug}/overview`);
if (!user) redirect(`/auth/sign-in?next=/w/${workspaceSlug}/projects/${projectSlug}/engineer`);

const [workspace, memberships] = await Promise.all([
prisma.workspace.findFirst({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,185 +1,15 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { ArrowRight, Combine, Cpu, Lightbulb, Rocket, ShieldCheck } from "lucide-react";
import { prisma } from "@foundry/db";
import { STAGE_LABELS, STAGES, type Stage } from "@foundry/domain";
import { MatrixScreen } from "@/components/matrix-cover";
import { SignalPageHeader } from "@/components/signal-page-header";
import { StatusBadge } from "@/components/status-badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { STAGE_THEME } from "@/lib/stage-theme";
import { cn } from "@/lib/utils";
import { PipelineKickoffListener } from "./pipeline-kickoff";

const STAGE_ICONS: Record<Stage, typeof Lightbulb> = {
IDEATE: Lightbulb,
ENGINEER: Cpu,
VERIFY: ShieldCheck,
LAUNCH: Rocket,
};

const STAGE_BLURBS: Record<Stage, string> = {
IDEATE: "Brief & requirements",
ENGINEER: "Assembly, CAD, circuit, PCB",
VERIFY: "Validation checklist",
LAUNCH: "Immutable releases",
};

const STAGE_CODES: Record<Stage, string> = {
IDEATE: "01",
ENGINEER: "02",
VERIFY: "03",
LAUNCH: "04",
};
import { redirect } from "next/navigation";

/**
* The workbench is the project. Overview used to be a stage-status landing
* page; every link now lands straight in the Engineer viewport, and old
* bookmarks follow.
*/
export default async function ProjectOverviewPage({
params,
}: {
params: Promise<{ workspaceSlug: string; projectSlug: string }>;
}) {
const { workspaceSlug, projectSlug } = await params;
const project = await prisma.project.findFirst({
where: { slug: projectSlug, workspace: { slug: workspaceSlug } },
include: { stageStates: true },
});
if (!project?.activeBranchId) notFound();

const [brief, recentEvents, counts] = await Promise.all([
prisma.projectBrief.findUnique({
where: {
projectId_branchId: { projectId: project.id, branchId: project.activeBranchId },
},
}),
prisma.auditEvent.findMany({
where: { projectId: project.id },
include: { actor: true },
orderBy: { createdAt: "desc" },
take: 12,
}),
Promise.all([
prisma.requirement.count({ where: { projectId: project.id } }),
prisma.component.count({ where: { projectId: project.id } }),
prisma.validationCheck.count({ where: { projectId: project.id } }),
prisma.release.count({ where: { projectId: project.id } }),
]),
]);
const [reqCount, compCount, checkCount, releaseCount] = counts;
const stageCounts: Record<Stage, number> = {
IDEATE: reqCount,
ENGINEER: compCount,
VERIFY: checkCount,
LAUNCH: releaseCount,
};

const base = `/w/${workspaceSlug}/projects/${projectSlug}`;

return (
<div className="mx-auto flex max-w-5xl flex-col gap-8 p-6 lg:p-8">
<SignalPageHeader
code="Project"
title={project.name}
subtitle={project.description}
glyphSeed={project.id}
/>

<PipelineKickoffListener hasBrief={Boolean(brief?.prompt || brief?.intendedUse)} />

<Link href={`${base}/engineer`} className="block">
<Card className="flex-row items-center gap-4 px-4 py-3.5 transition-colors hover:ring-foreground/30">
<span className="bg-primary text-primary-foreground relative flex size-10 shrink-0 items-center justify-center overflow-hidden">
<MatrixScreen color="#faf9f5" opacity={0.35} />
<Combine className="relative size-4" strokeWidth={1.75} />
</span>
<div className="min-w-0 flex-1">
<p className="font-mono text-[13px] font-medium tracking-[-0.02em]">
Assembly workspace
</p>
<p className="text-muted-foreground mt-0.5 font-mono text-[11px]">
Home viewport — CAD · Schematic · PCB as tabs
</p>
</div>
<ArrowRight className="text-muted-foreground size-3.5 shrink-0 transition-transform group-hover/card:translate-x-0.5" />
</Card>
</Link>

<div>
<p className="text-muted-foreground mb-2 font-mono text-[11px] tracking-[0.14em] uppercase">
Process — Engineer opens assembly
</p>
<div className="border-border bg-border grid grid-cols-2 gap-px border xl:grid-cols-4">
{STAGES.map((stage) => {
const state = project.stageStates.find(
(s) => s.stage === stage && s.branchId === project.activeBranchId,
);
const Icon = STAGE_ICONS[stage];
const phase = STAGE_THEME[stage];
return (
<Link key={stage} href={`${base}/${stage.toLowerCase()}`} className="block">
<Card
className={cn("h-full gap-3 px-4 py-4 ring-0 transition-colors", phase.cardHover)}
>
<div className="flex items-center justify-between gap-2">
<span className="text-muted-foreground font-mono text-[10px] tracking-[0.12em]">
{STAGE_CODES[stage]}
</span>
<StatusBadge
status={state?.status ?? "NOT_STARTED"}
className="rounded-none font-mono text-[10px] tracking-[0.04em]"
/>
</div>
<div className="flex items-center gap-2">
<Icon className={cn("size-3.5", phase.text)} strokeWidth={1.75} />
<p className="font-mono text-[13px] font-medium tracking-[-0.02em]">
{STAGE_LABELS[stage]}
</p>
</div>
<p className="text-muted-foreground font-mono text-[11px]">
{stageCounts[stage] > 0
? `${stageCounts[stage]} item${stageCounts[stage] === 1 ? "" : "s"}`
: STAGE_BLURBS[stage]}
</p>
</Card>
</Link>
);
})}
</div>
</div>

<Card className="gap-0 py-0">
<CardHeader className="border-b px-4 py-2.5">
<CardTitle className="text-muted-foreground font-mono text-[11px] font-medium tracking-[0.14em] uppercase">
Activity
</CardTitle>
</CardHeader>
<CardContent className="px-0">
{recentEvents.length === 0 ? (
<p className="text-muted-foreground px-4 py-6 font-mono text-[12px]">
No activity yet.
</p>
) : (
<ul className="divide-border divide-y">
{recentEvents.map((event) => (
<li
key={event.id}
className="flex items-center justify-between gap-3 px-4 py-2.5 text-[13px]"
>
<span className="min-w-0 truncate">
<span className="text-muted-foreground">
{event.actorType === "AGENT"
? "Copilot (as " + event.actor.name + ")"
: event.actor.name}
</span>{" "}
· {event.type.replace(/([a-z])([A-Z])/g, "$1 $2")}
</span>
<span className="text-muted-foreground shrink-0 font-mono text-[11px]">
{event.createdAt.toLocaleString()}
</span>
</li>
))}
</ul>
)}
</CardContent>
</Card>
</div>
);
redirect(`/w/${workspaceSlug}/projects/${projectSlug}/engineer`);
}
8 changes: 7 additions & 1 deletion apps/web/components/copilot/copilot-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,13 @@ function ChatEngine({
messages: seedRef.current,
onData: (chunk) => {
const progress = readCadProgress(chunk);
if (progress) cadProgressRef.current.set(progress);
if (progress) {
cadProgressRef.current.set(progress);
// A "saved" event means geometry just landed in the workspace mid-run:
// refetch so open viewports render it without waiting for the tool to
// finish. refreshProjectData is debounced, so bursts coalesce.
if (progress.phase === "saved") refreshProjectData();
}
},
// Manual resume only (see effect below). SDK auto-resume + our send SSE
// both attach to the same run and the UI flashes as chunks replay twice.
Expand Down
Loading
Loading