refactor: redesign Build page layout — sidebar nav+tree, agent as main, file viewer on right
- B (left sidebar, 260px): project header, Build pills (Code/Layouts/Infra), app list, file tree embedded below active app - D (center): AgentMode as primary content; sessions shown as a horizontal chip strip at the top instead of a 220px left sidebar - Right (460px): FileViewer — shows file selected in B's tree / code changes - F (bottom): Terminal collapsible strip unchanged - Split CodeContent into FileTree + FileViewer components; lifted file selection state to BuildHubInner so B and Right share it Made-with: Cursor
This commit is contained in:
@@ -395,35 +395,35 @@ function AgentMode({ projectId, appName, appPath }: { projectId: string; appName
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ flex: 1, display: "flex", overflow: "hidden" }}>
|
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||||
{/* Session list sidebar */}
|
{/* Horizontal sessions strip */}
|
||||||
<div style={{ width: 220, flexShrink: 0, borderRight: "1px solid #e8e4dc", background: "#faf8f5", display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
<div style={{ flexShrink: 0, height: 38, borderBottom: "1px solid #e8e4dc", background: "#faf8f5", display: "flex", alignItems: "center", gap: 8, padding: "0 16px", overflowX: "auto" }}>
|
||||||
<div style={{ padding: "10px 12px 8px", borderBottom: "1px solid #e8e4dc", fontSize: "0.65rem", fontWeight: 700, color: "#a09a90", letterSpacing: "0.08em", textTransform: "uppercase", fontFamily: "Outfit, sans-serif" }}>
|
<span style={{ fontSize: "0.6rem", fontWeight: 700, color: "#b5b0a6", letterSpacing: "0.09em", textTransform: "uppercase", whiteSpace: "nowrap", fontFamily: "Outfit, sans-serif" }}>Sessions</span>
|
||||||
Sessions
|
<span style={{ width: 1, height: 14, background: "#e0dcd5", flexShrink: 0 }} />
|
||||||
</div>
|
{loadingSessions && <span style={{ fontSize: "0.72rem", color: "#b5b0a6", fontFamily: "Outfit, sans-serif" }}>Loading…</span>}
|
||||||
<div style={{ flex: 1, overflow: "auto" }}>
|
|
||||||
{loadingSessions && <div style={{ padding: "12px", fontSize: "0.72rem", color: "#b5b0a6", fontFamily: "Outfit, sans-serif" }}>Loading…</div>}
|
|
||||||
{!loadingSessions && sessions.length === 0 && (
|
{!loadingSessions && sessions.length === 0 && (
|
||||||
<div style={{ padding: "16px 12px", fontSize: "0.73rem", color: "#b5b0a6", fontFamily: "Outfit, sans-serif", lineHeight: 1.5 }}>No sessions yet. Run your first task below.</div>
|
<span style={{ fontSize: "0.72rem", color: "#b5b0a6", fontFamily: "Outfit, sans-serif", whiteSpace: "nowrap" }}>No sessions yet — run your first task below</span>
|
||||||
)}
|
)}
|
||||||
{sessions.map(s => (
|
{sessions.map(s => (
|
||||||
<button key={s.id} onClick={() => { setActiveSessionId(s.id); setActiveSession(s); }} style={{
|
<button key={s.id} onClick={() => { setActiveSessionId(s.id); setActiveSession(s); }} style={{
|
||||||
width: "100%", textAlign: "left", padding: "10px 12px", border: "none", cursor: "pointer",
|
padding: "3px 10px", border: `1px solid ${activeSessionId === s.id ? "#1a1a1a" : "#e0dcd5"}`,
|
||||||
background: activeSessionId === s.id ? "#f0ece4" : "transparent",
|
borderRadius: 20, background: activeSessionId === s.id ? "#1a1a1a" : "transparent",
|
||||||
borderBottom: "1px solid #f0ece4",
|
color: activeSessionId === s.id ? "#fff" : "#5a5550",
|
||||||
}}
|
fontSize: "0.68rem", cursor: "pointer", whiteSpace: "nowrap",
|
||||||
onMouseEnter={e => { if (activeSessionId !== s.id) (e.currentTarget as HTMLElement).style.background = "#f6f4f0"; }}
|
display: "flex", alignItems: "center", gap: 5,
|
||||||
onMouseLeave={e => { if (activeSessionId !== s.id) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
|
fontFamily: "Outfit, sans-serif", flexShrink: 0,
|
||||||
>
|
}}>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 3 }}>
|
<span style={{ width: 5, height: 5, borderRadius: "50%", background: activeSessionId === s.id ? "#fff" : (STATUS_COLORS[s.status] ?? "#a09a90"), display: "inline-block", flexShrink: 0 }} />
|
||||||
<span style={{ width: 6, height: 6, borderRadius: "50%", background: STATUS_COLORS[s.status] ?? "#a09a90", flexShrink: 0, display: "inline-block" }} />
|
{s.task.length > 30 ? s.task.slice(0, 30) + "…" : s.task}
|
||||||
<span style={{ fontSize: "0.65rem", color: STATUS_COLORS[s.status] ?? "#a09a90", fontFamily: "Outfit, sans-serif", fontWeight: 600 }}>{STATUS_LABELS[s.status] ?? s.status}</span>
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: "0.73rem", color: "#1a1a1a", fontFamily: "Outfit, sans-serif", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{s.task}</div>
|
|
||||||
<div style={{ fontSize: "0.65rem", color: "#a09a90", marginTop: 2, fontFamily: "Outfit, sans-serif" }}>{new Date(s.created_at).toLocaleTimeString()}</div>
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
{!loadingSessions && sessions.length > 0 && (
|
||||||
|
<button onClick={() => { setActiveSession(null); setActiveSessionId(null); setTask(""); }} style={{
|
||||||
|
marginLeft: "auto", padding: "3px 10px", border: "1px solid #e0dcd5", borderRadius: 20,
|
||||||
|
background: "transparent", color: "#a09a90", fontSize: "0.68rem", cursor: "pointer",
|
||||||
|
fontFamily: "Outfit, sans-serif", whiteSpace: "nowrap", flexShrink: 0,
|
||||||
|
}}>+ New</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main panel */}
|
{/* Main panel */}
|
||||||
@@ -812,16 +812,15 @@ function TerminalPanel({ appName }: { appName: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Code content (file browser) ───────────────────────────────────────────────
|
// ── File tree (lives in sidebar B) ───────────────────────────────────────────
|
||||||
|
|
||||||
function CodeContent({ projectId, appName, rootPath }: { projectId: string; appName: string; rootPath: string }) {
|
function FileTree({ projectId, rootPath, selectedPath, onSelectFile }: {
|
||||||
|
projectId: string; rootPath: string; selectedPath: string | null;
|
||||||
|
onSelectFile: (path: string) => void;
|
||||||
|
}) {
|
||||||
const { status } = useSession();
|
const { status } = useSession();
|
||||||
const [tree, setTree] = useState<TreeNode[]>([]);
|
const [tree, setTree] = useState<TreeNode[]>([]);
|
||||||
const [treeLoading, setTreeLoading] = useState(false);
|
const [treeLoading, setTreeLoading] = useState(false);
|
||||||
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
|
||||||
const [fileContent, setFileContent] = useState<string | null>(null);
|
|
||||||
const [fileLoading, setFileLoading] = useState(false);
|
|
||||||
const [fileName, setFileName] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const fetchDir = useCallback(async (path: string): Promise<TreeNode[]> => {
|
const fetchDir = useCallback(async (path: string): Promise<TreeNode[]> => {
|
||||||
const res = await fetch(`/api/projects/${projectId}/file?path=${encodeURIComponent(path)}`);
|
const res = await fetch(`/api/projects/${projectId}/file?path=${encodeURIComponent(path)}`);
|
||||||
@@ -835,7 +834,7 @@ function CodeContent({ projectId, appName, rootPath }: { projectId: string; appN
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!rootPath || status !== "authenticated") return;
|
if (!rootPath || status !== "authenticated") return;
|
||||||
setTree([]); setSelectedPath(null); setFileContent(null); setTreeLoading(true);
|
setTree([]); setTreeLoading(true);
|
||||||
fetchDir(rootPath).then(nodes => { setTree(nodes); setTreeLoading(false); }).catch(() => setTreeLoading(false));
|
fetchDir(rootPath).then(nodes => { setTree(nodes); setTreeLoading(false); }).catch(() => setTreeLoading(false));
|
||||||
}, [rootPath, status, fetchDir]);
|
}, [rootPath, status, fetchDir]);
|
||||||
|
|
||||||
@@ -855,57 +854,53 @@ function CodeContent({ projectId, appName, rootPath }: { projectId: string; appN
|
|||||||
}
|
}
|
||||||
}, [tree, fetchDir]);
|
}, [tree, fetchDir]);
|
||||||
|
|
||||||
const handleSelectFile = useCallback(async (path: string) => {
|
|
||||||
setSelectedPath(path); setFileContent(null); setFileName(path.split("/").pop() ?? null); setFileLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/api/projects/${projectId}/file?path=${encodeURIComponent(path)}`);
|
|
||||||
const data = await res.json();
|
|
||||||
setFileContent(data.content ?? "");
|
|
||||||
} catch { setFileContent("// Failed to load"); }
|
|
||||||
finally { setFileLoading(false); }
|
|
||||||
}, [projectId]);
|
|
||||||
|
|
||||||
const lang = fileName ? langFromName(fileName) : "text";
|
|
||||||
const lines = (fileContent ?? "").split("\n");
|
|
||||||
|
|
||||||
if (!appName) {
|
|
||||||
return (
|
return (
|
||||||
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column", gap: 12, padding: 40, textAlign: "center" }}>
|
|
||||||
<div style={{ width: 48, height: 48, borderRadius: 12, background: "#f0ece4", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "1.3rem", color: "#b5b0a6" }}>▢</div>
|
|
||||||
<div>
|
|
||||||
<div style={{ fontSize: "0.88rem", fontWeight: 600, color: "#1a1a1a", marginBottom: 6, fontFamily: "Outfit, sans-serif" }}>Select an app</div>
|
|
||||||
<div style={{ fontSize: "0.78rem", color: "#a09a90", maxWidth: 240, lineHeight: 1.5, fontFamily: "Outfit, sans-serif" }}>Choose an app from the left to browse its source files.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ flex: 1, display: "flex", overflow: "hidden" }}>
|
|
||||||
{/* File tree */}
|
|
||||||
<div style={{ width: 200, flexShrink: 0, borderRight: "1px solid #e8e4dc", background: "#faf8f5", display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
|
||||||
<div style={{ padding: "10px 12px 8px", borderBottom: "1px solid #e8e4dc", display: "flex", alignItems: "center", gap: 6, flexShrink: 0 }}>
|
|
||||||
<span style={{ fontSize: "0.7rem", color: "#a09a90" }}>▢</span>
|
|
||||||
<span style={{ fontSize: "0.76rem", fontWeight: 600, color: "#1a1a1a", fontFamily: "Outfit, sans-serif" }}>{appName}</span>
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1, overflow: "auto", padding: "4px" }}>
|
<div style={{ flex: 1, overflow: "auto", padding: "4px" }}>
|
||||||
{treeLoading && <div style={{ padding: "12px", fontSize: "0.72rem", color: "#b5b0a6", fontFamily: "Outfit, sans-serif" }}>Loading…</div>}
|
{treeLoading && <div style={{ padding: "12px", fontSize: "0.72rem", color: "#b5b0a6", fontFamily: "Outfit, sans-serif" }}>Loading…</div>}
|
||||||
{!treeLoading && tree.length === 0 && <div style={{ padding: "12px", fontSize: "0.72rem", color: "#b5b0a6", fontFamily: "Outfit, sans-serif" }}>Empty.</div>}
|
{!treeLoading && tree.length === 0 && <div style={{ padding: "12px", fontSize: "0.72rem", color: "#b5b0a6", fontFamily: "Outfit, sans-serif" }}>Empty.</div>}
|
||||||
{tree.map(n => <TreeRow key={n.path} node={n} depth={0} selectedPath={selectedPath} onSelect={handleSelectFile} onToggle={handleToggle} />)}
|
{tree.map(n => <TreeRow key={n.path} node={n} depth={0} selectedPath={selectedPath} onSelect={onSelectFile} onToggle={handleToggle} />)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
{/* Code viewer */}
|
}
|
||||||
|
|
||||||
|
// ── File viewer (right panel) ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function FileViewer({ selectedPath, fileContent, fileLoading, fileName, rootPath }: {
|
||||||
|
selectedPath: string | null; fileContent: string | null;
|
||||||
|
fileLoading: boolean; fileName: string | null; rootPath: string;
|
||||||
|
}) {
|
||||||
|
const lang = fileName ? langFromName(fileName) : "text";
|
||||||
|
const lines = (fileContent ?? "").split("\n");
|
||||||
|
|
||||||
|
return (
|
||||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", background: "#1e1e1e", overflow: "hidden" }}>
|
<div style={{ flex: 1, display: "flex", flexDirection: "column", background: "#1e1e1e", overflow: "hidden" }}>
|
||||||
|
{/* File path header */}
|
||||||
<div style={{ padding: "9px 18px", borderBottom: "1px solid #2d2d2d", background: "#252526", display: "flex", alignItems: "center", gap: 8, flexShrink: 0 }}>
|
<div style={{ padding: "9px 18px", borderBottom: "1px solid #2d2d2d", background: "#252526", display: "flex", alignItems: "center", gap: 8, flexShrink: 0 }}>
|
||||||
{selectedPath ? (
|
{selectedPath ? (
|
||||||
<span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.71rem", color: "#a09a90" }}>
|
<span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.71rem", color: "#a09a90" }}>
|
||||||
{(() => { const rel = selectedPath.startsWith(rootPath + "/") ? selectedPath.slice(rootPath.length + 1) : selectedPath; return rel.split("/").map((s, i, a) => <span key={i}>{i > 0 && <span style={{ color: "#555", margin: "0 3px" }}>/</span>}<span style={{ color: i === a.length - 1 ? "#d4d4d4" : "#888" }}>{s}</span></span>); })()}
|
{(() => {
|
||||||
|
const rel = selectedPath.startsWith(rootPath + "/") ? selectedPath.slice(rootPath.length + 1) : selectedPath;
|
||||||
|
return rel.split("/").map((s, i, a) => (
|
||||||
|
<span key={i}>
|
||||||
|
{i > 0 && <span style={{ color: "#555", margin: "0 3px" }}>/</span>}
|
||||||
|
<span style={{ color: i === a.length - 1 ? "#d4d4d4" : "#888" }}>{s}</span>
|
||||||
</span>
|
</span>
|
||||||
) : <span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.71rem", color: "#555" }}>Select a file</span>}
|
));
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.71rem", color: "#555" }}>Select a file from the tree</span>
|
||||||
|
)}
|
||||||
{fileName && <span style={{ marginLeft: "auto", fontFamily: "IBM Plex Mono, monospace", fontSize: "0.62rem", color: "#555", textTransform: "uppercase" }}>{lang}</span>}
|
{fileName && <span style={{ marginLeft: "auto", fontFamily: "IBM Plex Mono, monospace", fontSize: "0.62rem", color: "#555", textTransform: "uppercase" }}>{lang}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Content */}
|
||||||
<div style={{ flex: 1, overflow: "auto", display: "flex" }}>
|
<div style={{ flex: 1, overflow: "auto", display: "flex" }}>
|
||||||
{!selectedPath && !fileLoading && <div style={{ display: "flex", alignItems: "center", justifyContent: "center", width: "100%", color: "#555", fontSize: "0.78rem", fontFamily: "IBM Plex Mono, monospace" }}>Select a file to view</div>}
|
{!selectedPath && !fileLoading && (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", width: "100%", flexDirection: "column", gap: 10, color: "#3a3a3a" }}>
|
||||||
|
<span style={{ fontSize: "1.6rem" }}>◌</span>
|
||||||
|
<span style={{ fontSize: "0.75rem", fontFamily: "IBM Plex Mono, monospace", color: "#555" }}>Select a file to view</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{fileLoading && <div style={{ display: "flex", alignItems: "center", justifyContent: "center", width: "100%", color: "#555", fontSize: "0.78rem", fontFamily: "IBM Plex Mono, monospace" }}>Loading…</div>}
|
{fileLoading && <div style={{ display: "flex", alignItems: "center", justifyContent: "center", width: "100%", color: "#555", fontSize: "0.78rem", fontFamily: "IBM Plex Mono, monospace" }}>Loading…</div>}
|
||||||
{!fileLoading && fileContent !== null && (
|
{!fileLoading && fileContent !== null && (
|
||||||
<div style={{ display: "flex", width: "100%" }}>
|
<div style={{ display: "flex", width: "100%" }}>
|
||||||
@@ -919,7 +914,6 @@ function CodeContent({ projectId, appName, rootPath }: { projectId: string; appN
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -941,6 +935,13 @@ function BuildHubInner() {
|
|||||||
const [apps, setApps] = useState<AppEntry[]>([]);
|
const [apps, setApps] = useState<AppEntry[]>([]);
|
||||||
const [surfaces, setSurfaces] = useState<SurfaceEntry[]>([]);
|
const [surfaces, setSurfaces] = useState<SurfaceEntry[]>([]);
|
||||||
const [activeSurfaceId, setActiveSurfaceId] = useState<string>(activeSurfaceParam);
|
const [activeSurfaceId, setActiveSurfaceId] = useState<string>(activeSurfaceParam);
|
||||||
|
const [projectName, setProjectName] = useState<string>("");
|
||||||
|
|
||||||
|
// File viewer state — lifted so FileTree (B) and FileViewer (right) share it
|
||||||
|
const [selectedFilePath, setSelectedFilePath] = useState<string | null>(null);
|
||||||
|
const [fileContent, setFileContent] = useState<string | null>(null);
|
||||||
|
const [fileLoading, setFileLoading] = useState(false);
|
||||||
|
const [fileName, setFileName] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`/api/projects/${projectId}/apps`).then(r => r.json()).then(d => setApps(d.apps ?? [])).catch(() => {});
|
fetch(`/api/projects/${projectId}/apps`).then(r => r.json()).then(d => setApps(d.apps ?? [])).catch(() => {});
|
||||||
@@ -950,10 +951,33 @@ function BuildHubInner() {
|
|||||||
setSurfaces(ids.map(id => ({ id, label: SURFACE_LABELS[id] ?? id, lockedTheme: themes[id] })));
|
setSurfaces(ids.map(id => ({ id, label: SURFACE_LABELS[id] ?? id, lockedTheme: themes[id] })));
|
||||||
if (!activeSurfaceId && ids.length > 0) setActiveSurfaceId(ids[0]);
|
if (!activeSurfaceId && ids.length > 0) setActiveSurfaceId(ids[0]);
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}, [projectId]);
|
// Best-effort project name fetch
|
||||||
|
fetch(`/api/projects/${projectId}/apps`).then(r => r.json())
|
||||||
|
.then(d => { if (d.projectName) setProjectName(d.projectName); }).catch(() => {});
|
||||||
|
}, [projectId]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const navigate = (params: Record<string, string>) => {
|
// Clear file selection when app changes
|
||||||
const sp = new URLSearchParams({ section, ...params });
|
useEffect(() => {
|
||||||
|
setSelectedFilePath(null);
|
||||||
|
setFileContent(null);
|
||||||
|
setFileName(null);
|
||||||
|
}, [activeApp]);
|
||||||
|
|
||||||
|
const handleSelectFile = async (path: string) => {
|
||||||
|
setSelectedFilePath(path);
|
||||||
|
setFileName(path.split("/").pop() ?? null);
|
||||||
|
setFileContent(null);
|
||||||
|
setFileLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/projects/${projectId}/file?path=${encodeURIComponent(path)}`);
|
||||||
|
const data = await res.json();
|
||||||
|
setFileContent(data.content ?? "");
|
||||||
|
} catch { setFileContent("// Failed to load"); }
|
||||||
|
finally { setFileLoading(false); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigate = (navParams: Record<string, string>) => {
|
||||||
|
const sp = new URLSearchParams({ section, ...navParams });
|
||||||
router.push(`/${workspace}/project/${projectId}/build?${sp.toString()}`, { scroll: false });
|
router.push(`/${workspace}/project/${projectId}/build?${sp.toString()}`, { scroll: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -962,71 +986,125 @@ function BuildHubInner() {
|
|||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", height: "100%", fontFamily: "Outfit, sans-serif", overflow: "hidden" }}>
|
<div style={{ display: "flex", height: "100%", fontFamily: "Outfit, sans-serif", overflow: "hidden" }}>
|
||||||
|
|
||||||
{/* ── Left nav ── */}
|
{/* ── B: Sidebar — project header + build pills + nav + file tree ── */}
|
||||||
<div style={{ width: 200, flexShrink: 0, borderRight: "1px solid #e8e4dc", background: "#faf8f5", display: "flex", flexDirection: "column", overflow: "auto" }}>
|
<div style={{ width: 260, flexShrink: 0, borderRight: "1px solid #e8e4dc", background: "#faf8f5", display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||||
|
|
||||||
{/* Code group */}
|
{/* Project header */}
|
||||||
<div style={NAV_GROUP_LABEL}>Code</div>
|
<div style={{ height: 48, flexShrink: 0, display: "flex", alignItems: "center", padding: "0 14px", borderBottom: "1px solid #e8e4dc", gap: 9 }}>
|
||||||
|
<span style={{ width: 26, height: 26, borderRadius: 7, background: "#1a1a1a", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "0.68rem", color: "#fff", flexShrink: 0, fontFamily: "Outfit, sans-serif" }}>◈</span>
|
||||||
|
<span style={{ fontSize: "0.82rem", fontWeight: 600, color: "#1a1a1a", fontFamily: "Outfit, sans-serif", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", flex: 1 }}>
|
||||||
|
{projectName || workspace}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Build section pills: Code | Layouts | Infra */}
|
||||||
|
<div style={{ padding: "10px 12px 9px", flexShrink: 0, borderBottom: "1px solid #f0ece4" }}>
|
||||||
|
<div style={{ fontSize: "0.57rem", fontWeight: 700, color: "#b5b0a6", letterSpacing: "0.1em", textTransform: "uppercase", marginBottom: 7, fontFamily: "Outfit, sans-serif" }}>Build</div>
|
||||||
|
<div style={{ display: "flex", gap: 5 }}>
|
||||||
|
{(["code", "layouts", "infrastructure"] as const).map(s => (
|
||||||
|
<button key={s} onClick={() => setSection(s)} style={{
|
||||||
|
padding: "4px 10px", border: "1px solid",
|
||||||
|
borderRadius: 20, fontSize: "0.68rem", fontWeight: section === s ? 600 : 440,
|
||||||
|
cursor: "pointer", fontFamily: "Outfit, sans-serif",
|
||||||
|
background: section === s ? "#1a1a1a" : "transparent",
|
||||||
|
color: section === s ? "#fff" : "#5a5550",
|
||||||
|
borderColor: section === s ? "#1a1a1a" : "#e0dcd5",
|
||||||
|
}}>
|
||||||
|
{s === "code" ? "Code" : s === "layouts" ? "Layouts" : "Infra"}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Code: app list + file tree */}
|
||||||
|
{section === "code" && (
|
||||||
|
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||||
|
{/* App list */}
|
||||||
|
<div style={{ flexShrink: 0 }}>
|
||||||
|
<div style={NAV_GROUP_LABEL}>Apps</div>
|
||||||
{apps.length > 0 ? apps.map(app => (
|
{apps.length > 0 ? apps.map(app => (
|
||||||
<NavItem key={app.name} label={app.name} indent
|
<NavItem key={app.name} label={app.name} indent
|
||||||
active={section === "code" && activeApp === app.name}
|
active={activeApp === app.name}
|
||||||
onClick={() => navigate({ section: "code", app: app.name, root: app.path })}
|
onClick={() => navigate({ section: "code", app: app.name, root: app.path })}
|
||||||
/>
|
/>
|
||||||
)) : (
|
)) : (
|
||||||
<NavItem label="No apps yet" indent active={section === "code" && !activeApp} onClick={() => setSection("code")} />
|
<div style={{ padding: "8px 22px", fontSize: "0.74rem", color: "#b5b0a6", fontFamily: "Outfit, sans-serif" }}>No apps yet</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File tree — appears below app list when an app is selected */}
|
||||||
|
{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: "Outfit, sans-serif" }}>Files</span>
|
||||||
|
<span style={{ fontSize: "0.65rem", 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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Layouts group */}
|
{/* Layouts: surface list */}
|
||||||
<div style={{ ...NAV_GROUP_LABEL, marginTop: 8 }}>Layouts</div>
|
{section === "layouts" && (
|
||||||
|
<div style={{ overflow: "auto", flex: 1 }}>
|
||||||
|
<div style={NAV_GROUP_LABEL}>Surfaces</div>
|
||||||
{surfaces.length > 0 ? surfaces.map(s => (
|
{surfaces.length > 0 ? surfaces.map(s => (
|
||||||
<NavItem key={s.id} label={SURFACE_LABELS[s.id] ?? s.id} indent
|
<NavItem key={s.id} label={SURFACE_LABELS[s.id] ?? s.id} indent
|
||||||
active={section === "layouts" && activeSurfaceId === s.id}
|
active={activeSurfaceId === s.id}
|
||||||
onClick={() => { setActiveSurfaceId(s.id); navigate({ section: "layouts", surface: s.id }); }}
|
onClick={() => { setActiveSurfaceId(s.id); navigate({ section: "layouts", surface: s.id }); }}
|
||||||
/>
|
/>
|
||||||
)) : (
|
)) : (
|
||||||
<NavItem label="Not configured" indent active={section === "layouts"} onClick={() => setSection("layouts")} />
|
<div style={{ padding: "8px 22px", fontSize: "0.74rem", color: "#b5b0a6", fontFamily: "Outfit, sans-serif" }}>Not configured</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Infrastructure group */}
|
{/* Infrastructure: item list */}
|
||||||
<div style={{ ...NAV_GROUP_LABEL, marginTop: 8 }}>Infrastructure</div>
|
{section === "infrastructure" && (
|
||||||
|
<div style={{ overflow: "auto", flex: 1 }}>
|
||||||
|
<div style={NAV_GROUP_LABEL}>Infrastructure</div>
|
||||||
{INFRA_ITEMS.map(item => (
|
{INFRA_ITEMS.map(item => (
|
||||||
<NavItem key={item.id} label={item.label} indent
|
<NavItem key={item.id} label={item.label} indent
|
||||||
active={section === "infrastructure" && activeInfra === item.id}
|
active={activeInfra === item.id}
|
||||||
onClick={() => navigate({ section: "infrastructure", tab: item.id })}
|
onClick={() => navigate({ section: "infrastructure", tab: item.id })}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ── Content ── */}
|
{/* ── Content ── */}
|
||||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden", minWidth: 0 }}>
|
<div style={{ flex: 1, display: "flex", overflow: "hidden", minWidth: 0 }}>
|
||||||
|
|
||||||
{/* Code section — persistent split: Browse (left) | Agent (right), Terminal (bottom) */}
|
{/* Code: D (agent chat) + Right (file viewer) + F (terminal) */}
|
||||||
{section === "code" && (
|
{section === "code" && (
|
||||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||||
{/* Main split row */}
|
|
||||||
<div style={{ flex: 1, display: "flex", overflow: "hidden" }}>
|
<div style={{ flex: 1, display: "flex", overflow: "hidden" }}>
|
||||||
{/* Left: Browse */}
|
|
||||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden", borderRight: "1px solid #e8e4dc", minWidth: 0 }}>
|
|
||||||
{/* Browse header */}
|
|
||||||
<div style={{ height: 32, flexShrink: 0, display: "flex", alignItems: "center", padding: "0 14px", borderBottom: "1px solid #e8e4dc", background: "#fff" }}>
|
|
||||||
<span style={{ fontSize: "0.68rem", fontWeight: 700, color: "#a09a90", letterSpacing: "0.07em", textTransform: "uppercase", fontFamily: "Outfit, sans-serif" }}>Browse</span>
|
|
||||||
{activeApp && <span style={{ marginLeft: 8, fontSize: "0.7rem", color: "#b5b0a6", fontFamily: "IBM Plex Mono, monospace" }}>{activeApp}</span>}
|
|
||||||
</div>
|
|
||||||
<CodeContent projectId={projectId} appName={activeApp} rootPath={activeRoot} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right: Agent */}
|
{/* D: Agent chat — main content */}
|
||||||
<div style={{ width: 420, flexShrink: 0, display: "flex", flexDirection: "column", overflow: "hidden", background: "#fff" }}>
|
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden", borderRight: "1px solid #e8e4dc", minWidth: 0 }}>
|
||||||
{/* Agent header */}
|
|
||||||
<div style={{ height: 32, flexShrink: 0, display: "flex", alignItems: "center", padding: "0 14px", borderBottom: "1px solid #e8e4dc" }}>
|
|
||||||
<span style={{ fontSize: "0.68rem", fontWeight: 700, color: "#a09a90", letterSpacing: "0.07em", textTransform: "uppercase", fontFamily: "Outfit, sans-serif" }}>Agent</span>
|
|
||||||
{activeApp && <span style={{ marginLeft: 8, fontSize: "0.7rem", color: "#b5b0a6", fontFamily: "IBM Plex Mono, monospace" }}>{activeApp}</span>}
|
|
||||||
</div>
|
|
||||||
<AgentMode projectId={projectId} appName={activeApp} appPath={activeRoot} />
|
<AgentMode projectId={projectId} appName={activeApp} appPath={activeRoot} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Right: File viewer — code changes stream here */}
|
||||||
|
<div style={{ width: 460, flexShrink: 0, display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
||||||
|
<FileViewer
|
||||||
|
selectedPath={selectedFilePath}
|
||||||
|
fileContent={fileContent}
|
||||||
|
fileLoading={fileLoading}
|
||||||
|
fileName={fileName}
|
||||||
|
rootPath={activeRoot}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom: Terminal (collapsible) */}
|
{/* F: Terminal strip */}
|
||||||
<TerminalPanel appName={activeApp} />
|
<TerminalPanel appName={activeApp} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user