From 86f8960aa32388a546fc0ce9785d8a1d8caa536d Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Mon, 9 Mar 2026 15:00:28 -0700 Subject: [PATCH] =?UTF-8?q?refactor:=20redesign=20Build=20page=20layout=20?= =?UTF-8?q?=E2=80=94=20sidebar=20nav+tree,=20agent=20as=20main,=20file=20v?= =?UTF-8?q?iewer=20on=20right?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../project/[projectId]/build/page.tsx | 362 +++++++++++------- 1 file changed, 220 insertions(+), 142 deletions(-) diff --git a/app/[workspace]/project/[projectId]/build/page.tsx b/app/[workspace]/project/[projectId]/build/page.tsx index 1cc0b2c..0be40a3 100644 --- a/app/[workspace]/project/[projectId]/build/page.tsx +++ b/app/[workspace]/project/[projectId]/build/page.tsx @@ -395,35 +395,35 @@ function AgentMode({ projectId, appName, appPath }: { projectId: string; appName } return ( -
- {/* Session list sidebar */} -
-
- Sessions -
-
- {loadingSessions &&
Loading…
} - {!loadingSessions && sessions.length === 0 && ( -
No sessions yet. Run your first task below.
- )} - {sessions.map(s => ( - - ))} -
+
+ {/* Horizontal sessions strip */} +
+ Sessions + + {loadingSessions && Loading…} + {!loadingSessions && sessions.length === 0 && ( + No sessions yet — run your first task below + )} + {sessions.map(s => ( + + ))} + {!loadingSessions && sessions.length > 0 && ( + + )}
{/* 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 [tree, setTree] = useState([]); const [treeLoading, setTreeLoading] = useState(false); - const [selectedPath, setSelectedPath] = useState(null); - const [fileContent, setFileContent] = useState(null); - const [fileLoading, setFileLoading] = useState(false); - const [fileName, setFileName] = useState(null); const fetchDir = useCallback(async (path: string): Promise => { const res = await fetch(`/api/projects/${projectId}/file?path=${encodeURIComponent(path)}`); @@ -835,7 +834,7 @@ function CodeContent({ projectId, appName, rootPath }: { projectId: string; appN useEffect(() => { 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)); }, [rootPath, status, fetchDir]); @@ -855,69 +854,64 @@ function CodeContent({ projectId, appName, rootPath }: { projectId: string; appN } }, [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]); + return ( +
+ {treeLoading &&
Loading…
} + {!treeLoading && tree.length === 0 &&
Empty.
} + {tree.map(n => )} +
+ ); +} +// ── 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"); - if (!appName) { - return ( -
-
-
-
Select an app
-
Choose an app from the left to browse its source files.
-
-
- ); - } - return ( -
- {/* File tree */} -
-
- - {appName} -
-
- {treeLoading &&
Loading…
} - {!treeLoading && tree.length === 0 &&
Empty.
} - {tree.map(n => )} -
+
+ {/* File path header */} +
+ {selectedPath ? ( + + {(() => { + const rel = selectedPath.startsWith(rootPath + "/") ? selectedPath.slice(rootPath.length + 1) : selectedPath; + return rel.split("/").map((s, i, a) => ( + + {i > 0 && /} + {s} + + )); + })()} + + ) : ( + Select a file from the tree + )} + {fileName && {lang}}
- {/* Code viewer */} -
-
- {selectedPath ? ( - - {(() => { const rel = selectedPath.startsWith(rootPath + "/") ? selectedPath.slice(rootPath.length + 1) : selectedPath; return rel.split("/").map((s, i, a) => {i > 0 && /}{s}); })()} - - ) : Select a file} - {fileName && {lang}} -
-
- {!selectedPath && !fileLoading &&
Select a file to view
} - {fileLoading &&
Loading…
} - {!fileLoading && fileContent !== null && ( -
-
- {lines.map((_, i) =>
{i + 1}
)} -
-
- {highlightCode(fileContent, lang)} -
+ {/* Content */} +
+ {!selectedPath && !fileLoading && ( +
+ + Select a file to view +
+ )} + {fileLoading &&
Loading…
} + {!fileLoading && fileContent !== null && ( +
+
+ {lines.map((_, i) =>
{i + 1}
)}
- )} -
+
+ {highlightCode(fileContent, lang)} +
+
+ )}
); @@ -941,6 +935,13 @@ function BuildHubInner() { const [apps, setApps] = useState([]); const [surfaces, setSurfaces] = useState([]); const [activeSurfaceId, setActiveSurfaceId] = useState(activeSurfaceParam); + const [projectName, setProjectName] = useState(""); + + // File viewer state — lifted so FileTree (B) and FileViewer (right) share it + const [selectedFilePath, setSelectedFilePath] = useState(null); + const [fileContent, setFileContent] = useState(null); + const [fileLoading, setFileLoading] = useState(false); + const [fileName, setFileName] = useState(null); useEffect(() => { 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] }))); if (!activeSurfaceId && ids.length > 0) setActiveSurfaceId(ids[0]); }).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) => { - const sp = new URLSearchParams({ section, ...params }); + // Clear file selection when app changes + 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) => { + const sp = new URLSearchParams({ section, ...navParams }); router.push(`/${workspace}/project/${projectId}/build?${sp.toString()}`, { scroll: false }); }; @@ -962,71 +986,125 @@ function BuildHubInner() { return (
- {/* ── Left nav ── */} -
+ {/* ── B: Sidebar — project header + build pills + nav + file tree ── */} +
- {/* Code group */} -
Code
- {apps.length > 0 ? apps.map(app => ( - navigate({ section: "code", app: app.name, root: app.path })} - /> - )) : ( - setSection("code")} /> + {/* Project header */} +
+ + + {projectName || workspace} + +
+ + {/* Build section pills: Code | Layouts | Infra */} +
+
Build
+
+ {(["code", "layouts", "infrastructure"] as const).map(s => ( + + ))} +
+
+ + {/* Code: app list + file tree */} + {section === "code" && ( +
+ {/* App list */} +
+
Apps
+ {apps.length > 0 ? apps.map(app => ( + navigate({ section: "code", app: app.name, root: app.path })} + /> + )) : ( +
No apps yet
+ )} +
+ + {/* File tree — appears below app list when an app is selected */} + {activeApp && activeRoot && ( +
+
+ Files + {activeApp} +
+ +
+ )} +
)} - {/* Layouts group */} -
Layouts
- {surfaces.length > 0 ? surfaces.map(s => ( - { setActiveSurfaceId(s.id); navigate({ section: "layouts", surface: s.id }); }} - /> - )) : ( - setSection("layouts")} /> + {/* Layouts: surface list */} + {section === "layouts" && ( +
+
Surfaces
+ {surfaces.length > 0 ? surfaces.map(s => ( + { setActiveSurfaceId(s.id); navigate({ section: "layouts", surface: s.id }); }} + /> + )) : ( +
Not configured
+ )} +
)} - {/* Infrastructure group */} -
Infrastructure
- {INFRA_ITEMS.map(item => ( - navigate({ section: "infrastructure", tab: item.id })} - /> - ))} + {/* Infrastructure: item list */} + {section === "infrastructure" && ( +
+
Infrastructure
+ {INFRA_ITEMS.map(item => ( + navigate({ section: "infrastructure", tab: item.id })} + /> + ))} +
+ )}
{/* ── Content ── */} -
+
- {/* Code section — persistent split: Browse (left) | Agent (right), Terminal (bottom) */} + {/* Code: D (agent chat) + Right (file viewer) + F (terminal) */} {section === "code" && (
- {/* Main split row */}
- {/* Left: Browse */} + + {/* D: Agent chat — main content */}
- {/* Browse header */} -
- Browse - {activeApp && {activeApp}} -
- +
- {/* Right: Agent */} -
- {/* Agent header */} -
- Agent - {activeApp && {activeApp}} -
- + {/* Right: File viewer — code changes stream here */} +
+
- {/* Bottom: Terminal (collapsible) */} + {/* F: Terminal strip */}
)}