- {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 */}
)}