fix(gitea-bot): add write:organization scope so bot can create repos
Without this the bot PAT 403s on POST /orgs/{org}/repos, which is
the single most important operation — creating new project repos
inside the workspace's Gitea org.
Made-with: Cursor
This commit is contained in:
@@ -3,7 +3,15 @@
|
||||
import { Suspense, useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useParams, useSearchParams, useRouter } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { isClientDevProjectBypass } from "@/lib/dev-bypass";
|
||||
import Link from "next/link";
|
||||
import { JM } from "@/components/project-creation/modal-theme";
|
||||
import { AtlasChat } from "@/components/AtlasChat";
|
||||
import { PRD_PLAN_SECTIONS } from "@/lib/prd-sections";
|
||||
import {
|
||||
type ChatContextRef,
|
||||
contextRefKey,
|
||||
} from "@/lib/chat-context-refs";
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -17,15 +25,6 @@ interface TreeNode {
|
||||
|
||||
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const INFRA_ITEMS = [
|
||||
{ id: "builds", label: "Builds", icon: "⬡" },
|
||||
{ id: "databases", label: "Databases", icon: "◫" },
|
||||
{ id: "services", label: "Services", icon: "◎" },
|
||||
{ id: "environment", label: "Environment", icon: "≡" },
|
||||
{ id: "domains", label: "Domains", icon: "◬" },
|
||||
{ id: "logs", label: "Logs", icon: "≈" },
|
||||
];
|
||||
|
||||
const SURFACE_LABELS: Record<string, string> = {
|
||||
webapp: "Web App", marketing: "Marketing Site", admin: "Admin Panel",
|
||||
};
|
||||
@@ -33,6 +32,74 @@ const SURFACE_ICONS: Record<string, string> = {
|
||||
webapp: "◈", marketing: "◌", admin: "◫",
|
||||
};
|
||||
|
||||
/** Growth page–style left rail: cream panel, icon + label rows */
|
||||
const BUILD_LEFT_BG = "#faf8f5";
|
||||
const BUILD_LEFT_BORDER = "#e8e4dc";
|
||||
|
||||
const BUILD_NAV_GROUP: React.CSSProperties = {
|
||||
fontSize: "0.6rem",
|
||||
fontWeight: 700,
|
||||
color: "#b5b0a6",
|
||||
letterSpacing: "0.09em",
|
||||
textTransform: "uppercase",
|
||||
padding: "14px 12px 6px",
|
||||
fontFamily: JM.fontSans,
|
||||
};
|
||||
|
||||
const BUILD_PRIMARY = [
|
||||
{ id: "chat", label: "Chat", icon: "◆" },
|
||||
{ id: "code", label: "Code", icon: "◇" },
|
||||
{ id: "layouts", label: "Layouts", icon: "◈" },
|
||||
{ id: "tasks", label: "Agent", icon: "◎" },
|
||||
{ id: "preview", label: "Preview", icon: "▢" },
|
||||
] as const;
|
||||
|
||||
function BuildGrowthNavRow({
|
||||
icon,
|
||||
label,
|
||||
active,
|
||||
onClick,
|
||||
}: {
|
||||
icon: string;
|
||||
label: string;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
background: active ? "#f0ece4" : "transparent",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
padding: "6px 12px",
|
||||
borderRadius: 5,
|
||||
fontSize: "0.78rem",
|
||||
fontWeight: active ? 600 : 440,
|
||||
color: active ? "#1a1a1a" : "#5a5550",
|
||||
fontFamily: JM.fontSans,
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
if (!active) (e.currentTarget as HTMLElement).style.background = "#f6f4f0";
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
if (!active) (e.currentTarget as HTMLElement).style.background = "transparent";
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: "0.65rem", opacity: 0.55, width: 14, textAlign: "center", flexShrink: 0 }}>
|
||||
{icon}
|
||||
</span>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Language / syntax helpers ─────────────────────────────────────────────────
|
||||
|
||||
function langFromName(name: string): string {
|
||||
@@ -103,28 +170,11 @@ function TreeRow({ node, depth, selectedPath, onSelect, onToggle }: {
|
||||
// ── Left nav shared styles ────────────────────────────────────────────────────
|
||||
|
||||
const NAV_GROUP_LABEL: React.CSSProperties = {
|
||||
fontSize: "0.6rem", fontWeight: 700, color: "#b5b0a6",
|
||||
fontSize: "0.6rem", fontWeight: 700, color: JM.muted,
|
||||
letterSpacing: "0.09em", textTransform: "uppercase",
|
||||
padding: "12px 12px 5px", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
|
||||
padding: "12px 12px 5px", fontFamily: JM.fontSans,
|
||||
};
|
||||
|
||||
function NavItem({ label, active, onClick, indent = false }: { label: string; active: boolean; onClick: () => void; indent?: boolean }) {
|
||||
return (
|
||||
<button onClick={onClick} style={{
|
||||
display: "flex", alignItems: "center", gap: 7, width: "100%", textAlign: "left",
|
||||
background: active ? "#f0ece4" : "transparent", border: "none", cursor: "pointer",
|
||||
padding: `5px 12px 5px ${indent ? 22 : 12}px`, borderRadius: 5,
|
||||
fontSize: "0.78rem", fontWeight: active ? 600 : 440,
|
||||
color: active ? "#1a1a1a" : "#5a5550", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
|
||||
}}
|
||||
onMouseEnter={e => { if (!active) (e.currentTarget as HTMLElement).style.background = "#f6f4f0"; }}
|
||||
onMouseLeave={e => { if (!active) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Placeholder panel ─────────────────────────────────────────────────────────
|
||||
|
||||
function Placeholder({ icon, title, desc }: { icon: string; title: string; desc: string }) {
|
||||
@@ -140,30 +190,6 @@ function Placeholder({ icon, title, desc }: { icon: string; title: string; desc:
|
||||
);
|
||||
}
|
||||
|
||||
// ── Infra content ─────────────────────────────────────────────────────────────
|
||||
|
||||
function InfraContent({ tab, projectId, workspace }: { tab: string; projectId: string; workspace: string }) {
|
||||
const base = `/${workspace}/project/${projectId}/infrastructure`;
|
||||
const descriptions: Record<string, { icon: string; title: string; desc: string }> = {
|
||||
databases: { icon: "◫", title: "Databases", desc: "PostgreSQL, Redis, and other databases — provisioned and managed with connection strings auto-injected." },
|
||||
services: { icon: "◎", title: "Services", desc: "Background workers, queues, email delivery, file storage, and third-party integrations." },
|
||||
environment: { icon: "≡", title: "Environment", desc: "Environment variables and secrets, encrypted at rest and auto-injected into your containers." },
|
||||
domains: { icon: "◬", title: "Domains", desc: "Custom domains and SSL certificates for all your deployed services." },
|
||||
logs: { icon: "≈", title: "Logs", desc: "Runtime logs, request traces, and error reports streaming from deployed services." },
|
||||
builds: { icon: "⬡", title: "Builds", desc: "Deployment history, build logs, and rollback controls for all your apps." },
|
||||
};
|
||||
const d = descriptions[tab];
|
||||
return (
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
|
||||
<div style={{ padding: "20px 28px 0", display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<div style={{ fontSize: "0.68rem", fontWeight: 700, color: "#a09a90", letterSpacing: "0.08em", textTransform: "uppercase", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif" }}>{tab}</div>
|
||||
<Link href={`${base}?tab=${tab}`} style={{ fontSize: "0.72rem", color: "#a09a90", textDecoration: "none", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif" }}>Open full view →</Link>
|
||||
</div>
|
||||
{d && <Placeholder icon={d.icon} title={d.title} desc={d.desc} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Layouts content ───────────────────────────────────────────────────────────
|
||||
|
||||
function LayoutsContent({ surfaces, projectId, workspace, activeSurfaceId, onSelectSurface }: {
|
||||
@@ -956,7 +982,8 @@ function FileTree({ projectId, rootPath, selectedPath, onSelectFile }: {
|
||||
}, [projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rootPath || status !== "authenticated") return;
|
||||
if (!rootPath) return;
|
||||
if (!isClientDevProjectBypass() && status !== "authenticated") return;
|
||||
setTree([]); setTreeLoading(true);
|
||||
fetchDir(rootPath).then(nodes => { setTree(nodes); setTreeLoading(false); }).catch(() => setTreeLoading(false));
|
||||
}, [rootPath, status, fetchDir]);
|
||||
@@ -1046,21 +1073,6 @@ interface PreviewApp { name: string; url: string | null; status: string; }
|
||||
|
||||
// ── PRD Content ───────────────────────────────────────────────────────────────
|
||||
|
||||
const PRD_SECTIONS = [
|
||||
{ id: "executive_summary", label: "Executive Summary", phaseId: "big_picture" },
|
||||
{ id: "problem_statement", label: "Problem Statement", phaseId: "big_picture" },
|
||||
{ id: "vision_metrics", label: "Vision & Success Metrics", phaseId: "big_picture" },
|
||||
{ id: "users_personas", label: "Users & Personas", phaseId: "users_personas" },
|
||||
{ id: "user_flows", label: "User Flows", phaseId: "users_personas" },
|
||||
{ id: "feature_requirements", label: "Feature Requirements", phaseId: "features_scope" },
|
||||
{ id: "screen_specs", label: "Screen Specs", phaseId: "screens_data" },
|
||||
{ id: "business_model", label: "Business Model", phaseId: "business_model" },
|
||||
{ id: "integrations", label: "Integrations & Dependencies", phaseId: "features_scope" },
|
||||
{ id: "non_functional", label: "Non-Functional Reqs", phaseId: null },
|
||||
{ id: "risks", label: "Risks & Mitigations", phaseId: "risks_questions" },
|
||||
{ id: "open_questions", label: "Open Questions", phaseId: "risks_questions" },
|
||||
];
|
||||
|
||||
interface SavedPhase { phase: string; title: string; summary: string; data: Record<string, unknown>; saved_at: string; }
|
||||
|
||||
function PrdContent({ projectId }: { projectId: string }) {
|
||||
@@ -1086,7 +1098,7 @@ function PrdContent({ projectId }: { projectId: string }) {
|
||||
|
||||
const phaseMap = new Map(savedPhases.map(p => [p.phase, p]));
|
||||
const savedPhaseIds = new Set(savedPhases.map(p => p.phase));
|
||||
const sections = PRD_SECTIONS.map(s => ({
|
||||
const sections = PRD_PLAN_SECTIONS.map(s => ({
|
||||
...s,
|
||||
savedPhase: s.phaseId ? phaseMap.get(s.phaseId) ?? null : null,
|
||||
isDone: s.phaseId ? savedPhaseIds.has(s.phaseId) : false,
|
||||
@@ -1272,10 +1284,11 @@ function BuildHubInner() {
|
||||
const projectId = params.projectId as string;
|
||||
const workspace = params.workspace as string;
|
||||
|
||||
const section = searchParams.get("section") ?? "code";
|
||||
const section = searchParams.get("section") ?? "chat";
|
||||
const tasksSubTabRaw = searchParams.get("tab") ?? "tasks";
|
||||
const tasksSubTab = tasksSubTabRaw === "prd" ? "requirements" : tasksSubTabRaw;
|
||||
const activeApp = searchParams.get("app") ?? "";
|
||||
const activeRoot = searchParams.get("root") ?? "";
|
||||
const activeInfra = searchParams.get("tab") ?? "builds";
|
||||
const activeSurfaceParam = searchParams.get("surface") ?? "";
|
||||
|
||||
const [apps, setApps] = useState<AppEntry[]>([]);
|
||||
@@ -1292,6 +1305,32 @@ function BuildHubInner() {
|
||||
const [fileLoading, setFileLoading] = useState(false);
|
||||
const [fileName, setFileName] = useState<string | null>(null);
|
||||
|
||||
const [chatContextRefs, setChatContextRefs] = useState<ChatContextRef[]>([]);
|
||||
|
||||
const addAppToChat = useCallback((app: AppEntry) => {
|
||||
setChatContextRefs(prev => {
|
||||
const next: ChatContextRef = { kind: "app", label: app.name, path: app.path };
|
||||
const k = contextRefKey(next);
|
||||
if (prev.some(r => contextRefKey(r) === k)) return prev;
|
||||
return [...prev, next];
|
||||
});
|
||||
}, []);
|
||||
|
||||
const removeChatContextRef = useCallback((key: string) => {
|
||||
setChatContextRefs(prev => prev.filter(r => contextRefKey(r) !== key));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get("section") !== "infrastructure") return;
|
||||
const t = searchParams.get("tab") ?? "builds";
|
||||
router.replace(`/${workspace}/project/${projectId}/run?tab=${encodeURIComponent(t)}`, { scroll: false });
|
||||
}, [searchParams, workspace, projectId, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get("section") !== "mvp") return;
|
||||
router.replace(`/${workspace}/project/${projectId}/mvp-setup/launch`, { scroll: false });
|
||||
}, [searchParams, workspace, projectId, router]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/projects/${projectId}/preview-url`)
|
||||
.then(r => r.json())
|
||||
@@ -1336,82 +1375,114 @@ function BuildHubInner() {
|
||||
router.push(`/${workspace}/project/${projectId}/build?${sp.toString()}`, { scroll: false });
|
||||
};
|
||||
|
||||
const workspaceAppActive = (app: AppEntry) => {
|
||||
if (section === "chat") {
|
||||
return chatContextRefs.some(r => r.kind === "app" && r.path === app.path);
|
||||
}
|
||||
if (section === "code" || (section === "tasks" && tasksSubTab !== "requirements")) {
|
||||
return activeApp === app.name;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const onWorkspaceApp = (app: AppEntry) => {
|
||||
if (section === "chat") addAppToChat(app);
|
||||
else if (section === "code") navigate({ section: "code", app: app.name, root: app.path });
|
||||
else if (section === "tasks" && tasksSubTab !== "requirements") {
|
||||
navigate({ section: "tasks", tab: "tasks", app: app.name, root: app.path });
|
||||
}
|
||||
else navigate({ section: "code", app: app.name, root: app.path });
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", height: "100%", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", overflow: "hidden" }}>
|
||||
<div style={{ display: "flex", height: "100%", fontFamily: JM.fontSans, overflow: "hidden", background: JM.inputBg }}>
|
||||
{/* Growth-style left rail */}
|
||||
<div style={{
|
||||
width: 200,
|
||||
flexShrink: 0,
|
||||
borderRight: `1px solid ${BUILD_LEFT_BORDER}`,
|
||||
background: BUILD_LEFT_BG,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<div style={BUILD_NAV_GROUP}>Build</div>
|
||||
{BUILD_PRIMARY.map(p => (
|
||||
<BuildGrowthNavRow
|
||||
key={p.id}
|
||||
icon={p.icon}
|
||||
label={p.label}
|
||||
active={section === p.id}
|
||||
onClick={() => {
|
||||
if (p.id === "tasks") navigate({ section: "tasks", tab: "tasks" });
|
||||
else navigate({ section: p.id });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* ── Build content ── */}
|
||||
<div style={{ flex: 1, display: "flex", overflow: "hidden", minWidth: 0 }}>
|
||||
|
||||
{/* Inner nav — contextual items driven by top-bar tool icon */}
|
||||
<div style={{ width: 200, flexShrink: 0, borderRight: "1px solid #e8e4dc", background: "#faf8f5", display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||
|
||||
{/* Code: app list + file tree */}
|
||||
{section === "code" && (
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
<div style={NAV_GROUP_LABEL}>Apps</div>
|
||||
{apps.length > 0 ? apps.map(app => (
|
||||
<NavItem key={app.name} label={app.name} indent
|
||||
active={activeApp === app.name}
|
||||
onClick={() => navigate({ section: "code", app: app.name, root: app.path })}
|
||||
/>
|
||||
)) : (
|
||||
<div style={{ padding: "8px 22px", fontSize: "0.74rem", color: "#b5b0a6", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif" }}>No apps yet</div>
|
||||
)}
|
||||
</div>
|
||||
{activeApp && activeRoot && (
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden", borderTop: "1px solid #e8e4dc", marginTop: 6 }}>
|
||||
<div style={{ padding: "7px 12px 4px", flexShrink: 0, display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<span style={{ fontSize: "0.57rem", fontWeight: 700, color: "#b5b0a6", letterSpacing: "0.1em", textTransform: "uppercase", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif" }}>Files</span>
|
||||
<span style={{ fontSize: "0.62rem", color: "#a09a90", fontFamily: "IBM Plex Mono, monospace", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{activeApp}</span>
|
||||
</div>
|
||||
<FileTree projectId={projectId} rootPath={activeRoot} selectedPath={selectedFilePath} onSelectFile={handleSelectFile} />
|
||||
</div>
|
||||
)}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflowY: "auto",
|
||||
borderTop: `1px solid ${BUILD_LEFT_BORDER}`,
|
||||
marginTop: 6,
|
||||
paddingTop: 4,
|
||||
}}>
|
||||
{section === "chat" && (
|
||||
<div style={{ padding: "8px 12px 12px", fontSize: 11, color: JM.mid, lineHeight: 1.45, fontFamily: JM.fontSans }}>
|
||||
Attach monorepo apps from <strong style={{ fontWeight: 600 }}>Workspace</strong> below. Live preview stays on the right when deployed.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{section === "code" && activeApp && activeRoot && (
|
||||
<div style={{ display: "flex", flexDirection: "column", paddingBottom: 10, borderBottom: `1px solid ${BUILD_LEFT_BORDER}`, marginBottom: 8 }}>
|
||||
<div style={NAV_GROUP_LABEL}>Files</div>
|
||||
<div style={{
|
||||
padding: "2px 12px 6px",
|
||||
fontSize: "0.62rem",
|
||||
color: JM.mid,
|
||||
fontFamily: "IBM Plex Mono, monospace",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}>
|
||||
{activeApp}
|
||||
</div>
|
||||
<div style={{ flex: 1, minHeight: 0, maxHeight: 280, overflow: "auto" }}>
|
||||
<FileTree projectId={projectId} rootPath={activeRoot} selectedPath={selectedFilePath} onSelectFile={handleSelectFile} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Layouts: surface list */}
|
||||
{section === "layouts" && (
|
||||
<div style={{ overflow: "auto", flex: 1 }}>
|
||||
<div style={{ paddingBottom: 8 }}>
|
||||
<div style={NAV_GROUP_LABEL}>Surfaces</div>
|
||||
{surfaces.length > 0 ? surfaces.map(s => (
|
||||
<NavItem key={s.id} label={SURFACE_LABELS[s.id] ?? s.id} indent
|
||||
<BuildGrowthNavRow
|
||||
key={s.id}
|
||||
icon={SURFACE_ICONS[s.id] ?? "◈"}
|
||||
label={SURFACE_LABELS[s.id] ?? s.id}
|
||||
active={activeSurfaceId === s.id}
|
||||
onClick={() => { setActiveSurfaceId(s.id); navigate({ section: "layouts", surface: s.id }); }}
|
||||
/>
|
||||
)) : (
|
||||
<div style={{ padding: "8px 22px", fontSize: "0.74rem", color: "#b5b0a6", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif" }}>Not configured</div>
|
||||
<div style={{ padding: "8px 12px", fontSize: "0.74rem", color: JM.muted, fontFamily: JM.fontSans }}>Not configured</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Infrastructure: item list */}
|
||||
{section === "infrastructure" && (
|
||||
<div style={{ overflow: "auto", flex: 1 }}>
|
||||
<div style={NAV_GROUP_LABEL}>Infrastructure</div>
|
||||
{INFRA_ITEMS.map(item => (
|
||||
<NavItem key={item.id} label={item.label} indent
|
||||
active={activeInfra === item.id}
|
||||
onClick={() => navigate({ section: "infrastructure", tab: item.id })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tasks: sub-nav + app list */}
|
||||
{section === "tasks" && (
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||
{/* Tasks | PRD sub-nav */}
|
||||
<div style={{ flexShrink: 0, padding: "8px 10px", borderBottom: "1px solid #e8e4dc", display: "flex", gap: 4 }}>
|
||||
{[{ id: "tasks", label: "Tasks" }, { id: "prd", label: "PRD" }].map(item => {
|
||||
const isActive = (searchParams.get("tab") ?? "tasks") === item.id;
|
||||
<div style={{ paddingBottom: 8 }}>
|
||||
<div style={{ padding: "8px 10px", borderBottom: `1px solid ${BUILD_LEFT_BORDER}`, display: "flex", gap: 4, marginBottom: 8 }}>
|
||||
{[{ id: "tasks", label: "Runs" }, { id: "requirements", label: "Requirements" }].map(item => {
|
||||
const isActive = tasksSubTab === item.id;
|
||||
return (
|
||||
<button key={item.id} onClick={() => navigate({ section: "tasks", tab: item.id })} style={{
|
||||
<button key={item.id} type="button" onClick={() => navigate({ section: "tasks", tab: item.id })} style={{
|
||||
flex: 1, padding: "5px 0", border: "none", borderRadius: 6, cursor: "pointer",
|
||||
fontSize: "0.72rem", fontWeight: 600, fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
|
||||
background: isActive ? "#1a1a1a" : "transparent",
|
||||
color: isActive ? "#fff" : "#a09a90",
|
||||
fontSize: "0.72rem", fontWeight: 600, fontFamily: JM.fontSans,
|
||||
background: isActive ? JM.primaryGradient : "transparent",
|
||||
color: isActive ? "#fff" : JM.mid,
|
||||
boxShadow: isActive ? JM.primaryShadow : "none",
|
||||
transition: "all 0.12s",
|
||||
}}>
|
||||
{item.label}
|
||||
@@ -1419,40 +1490,106 @@ function BuildHubInner() {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* App list (only in tasks tab) */}
|
||||
{(searchParams.get("tab") ?? "tasks") !== "prd" && (
|
||||
<>
|
||||
<div style={NAV_GROUP_LABEL}>Apps</div>
|
||||
{apps.length > 0 ? apps.map(app => (
|
||||
<NavItem key={app.name} label={app.name} indent
|
||||
active={activeApp === app.name}
|
||||
onClick={() => navigate({ section: "tasks", tab: "tasks", app: app.name, root: app.path })}
|
||||
/>
|
||||
)) : (
|
||||
<div style={{ padding: "8px 22px", fontSize: "0.74rem", color: "#b5b0a6", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif" }}>No apps yet</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Preview: deployed apps list */}
|
||||
{section === "preview" && (
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||
<div style={NAV_GROUP_LABEL}>Apps</div>
|
||||
<div style={{ paddingBottom: 8 }}>
|
||||
<div style={NAV_GROUP_LABEL}>Deployed</div>
|
||||
{previewApps.length > 0 ? previewApps.map(app => (
|
||||
<NavItem key={app.name} label={app.name} indent
|
||||
<BuildGrowthNavRow
|
||||
key={app.name}
|
||||
icon="▢"
|
||||
label={app.name}
|
||||
active={activePreviewApp?.name === app.name}
|
||||
onClick={() => setActivePreviewApp(app)}
|
||||
/>
|
||||
)) : (
|
||||
<div style={{ padding: "8px 22px", fontSize: "0.74rem", color: "#b5b0a6", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif" }}>No deployments yet</div>
|
||||
<div style={{ padding: "8px 12px", fontSize: "0.74rem", color: JM.muted, fontFamily: JM.fontSans }}>No deployments yet</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Main content panel */}
|
||||
<div style={{ flexShrink: 0, borderTop: `1px solid ${BUILD_LEFT_BORDER}`, paddingBottom: 10, background: BUILD_LEFT_BG }}>
|
||||
{apps.length > 0 && (
|
||||
<>
|
||||
<div style={BUILD_NAV_GROUP}>Workspace</div>
|
||||
{apps.map(app => (
|
||||
<BuildGrowthNavRow
|
||||
key={app.name}
|
||||
icon="◈"
|
||||
label={app.name}
|
||||
active={workspaceAppActive(app)}
|
||||
onClick={() => onWorkspaceApp(app)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{section === "chat" && previewApps.length > 0 && (
|
||||
<>
|
||||
<div style={BUILD_NAV_GROUP}>Live</div>
|
||||
{previewApps.map(app => (
|
||||
<BuildGrowthNavRow
|
||||
key={`live-${app.name}`}
|
||||
icon="▢"
|
||||
label={app.name}
|
||||
active={activePreviewApp?.name === app.name}
|
||||
onClick={() => setActivePreviewApp(app)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<Link
|
||||
href={`/${workspace}/project/${projectId}/tasks`}
|
||||
style={{
|
||||
display: "block",
|
||||
margin: "10px 12px 0",
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: JM.indigo,
|
||||
textDecoration: "none",
|
||||
fontFamily: JM.fontSans,
|
||||
}}
|
||||
>
|
||||
Open Tasks →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div style={{ flex: 1, display: "flex", overflow: "hidden", minWidth: 0 }}>
|
||||
{section === "chat" && (
|
||||
<div style={{ flex: 1, display: "flex", minWidth: 0, overflow: "hidden" }}>
|
||||
<div style={{ flex: 1, minWidth: 0, minHeight: 0 }}>
|
||||
<AtlasChat
|
||||
projectId={projectId}
|
||||
conversationScope="build"
|
||||
contextEmptyLabel="Workspace"
|
||||
emptyStateHint="Plan and implement in your monorepo. Attach apps from the left, preview on the right when deployed."
|
||||
chatContextRefs={chatContextRefs}
|
||||
onRemoveChatContextRef={removeChatContextRef}
|
||||
/>
|
||||
</div>
|
||||
<div style={{
|
||||
width: 400,
|
||||
flexShrink: 0,
|
||||
minWidth: 280,
|
||||
borderLeft: `1px solid ${JM.border}`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
background: JM.inputBg,
|
||||
}}>
|
||||
<PreviewContent
|
||||
projectId={projectId}
|
||||
apps={previewApps}
|
||||
activePreviewApp={activePreviewApp}
|
||||
onSelectApp={setActivePreviewApp}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{section === "code" && (
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||
<FileViewer
|
||||
@@ -1468,13 +1605,10 @@ function BuildHubInner() {
|
||||
{section === "layouts" && (
|
||||
<LayoutsContent surfaces={surfaces} projectId={projectId} workspace={workspace} activeSurfaceId={activeSurfaceId} onSelectSurface={id => { setActiveSurfaceId(id); navigate({ section: "layouts", surface: id }); }} />
|
||||
)}
|
||||
{section === "infrastructure" && (
|
||||
<InfraContent tab={activeInfra} projectId={projectId} workspace={workspace} />
|
||||
)}
|
||||
{section === "tasks" && (searchParams.get("tab") ?? "tasks") !== "prd" && (
|
||||
{section === "tasks" && tasksSubTab !== "requirements" && (
|
||||
<AgentMode projectId={projectId} appName={activeApp} appPath={activeRoot} />
|
||||
)}
|
||||
{section === "tasks" && searchParams.get("tab") === "prd" && (
|
||||
{section === "tasks" && tasksSubTab === "requirements" && (
|
||||
<PrdContent projectId={projectId} />
|
||||
)}
|
||||
{section === "preview" && (
|
||||
@@ -1492,7 +1626,7 @@ function BuildHubInner() {
|
||||
|
||||
export default function BuildPage() {
|
||||
return (
|
||||
<Suspense fallback={<div style={{ display: "flex", height: "100%", alignItems: "center", justifyContent: "center", color: "#a09a90", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", fontSize: "0.85rem" }}>Loading…</div>}>
|
||||
<Suspense fallback={<div style={{ display: "flex", height: "100%", alignItems: "center", justifyContent: "center", color: JM.muted, fontFamily: JM.fontSans, fontSize: "0.85rem" }}>Loading…</div>}>
|
||||
<BuildHubInner />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user