diff --git a/graph-ui/src/App.tsx b/graph-ui/src/App.tsx
index 2742da707..d79f85bae 100644
--- a/graph-ui/src/App.tsx
+++ b/graph-ui/src/App.tsx
@@ -9,6 +9,7 @@ import { FlowsTab } from "./components/FlowsTab";
import { ChangesTab } from "./components/ChangesTab";
import { DashboardTab } from "./components/DashboardTab";
import { CommandK } from "./components/CommandK";
+import { ProjectSwitcher } from "./components/ProjectSwitcher";
import {
PromptBasketProvider,
PromptBasketDrawer,
@@ -29,7 +30,6 @@ const TAB_IDS: TabId[] = [
];
/* Tabs that need a selected project. "symbol" is reachable only via links. */
-const PROJECT_TABS: TabId[] = ["overview", "modules", "graph", "flows", "changes", "dashboard"];
interface RouteState {
tab: TabId;
@@ -140,15 +140,16 @@ export function App() {
[navigate],
);
- const tabs: { id: TabId; label: string }[] = [
+ /* Project-scoped views only — they render once a project is chosen.
+ * App-level destinations (Projects, Control) live on the right instead of
+ * being mixed into this row as dead buttons. */
+ const projectTabs: { id: TabId; label: string }[] = [
{ id: "overview", label: "Overview" },
{ id: "modules", label: "Modules" },
{ id: "graph", label: "Galaxy" },
{ id: "flows", label: "Flows" },
{ id: "changes", label: "Changes" },
{ id: "dashboard", label: "Dashboard" },
- { id: "stats", label: t.tabs.projects },
- { id: "control", label: t.tabs.control },
];
const symbolRef = route.sym
@@ -162,7 +163,7 @@ export function App() {
{/* Header */}
-
+
@@ -170,56 +171,56 @@ export function App() {
-
+ >
+ )}
-
setCommandOpen(true)}
- className="px-2.5 py-1 rounded-md text-[13px] text-muted-foreground hover:text-foreground bg-popover hover:bg-surface-3 border border-border/40 transition-all"
- title="Search symbols and code"
- >
- ⌘K
-
{selectedProject && (
-
-
- {selectedProject}
-
- navigate({ tab: "stats", project: null })}
- className="text-foreground/35 hover:text-foreground/50 text-[12px] transition-colors"
- >
- ×
-
-
+
setCommandOpen(true)}
+ className="px-2.5 py-1 rounded-md text-[13px] text-muted-foreground hover:text-foreground bg-popover hover:bg-surface-3 border border-border/40 transition-all"
+ title="Search symbols and code"
+ >
+ ⌘K
+
)}
+
navigate({ tab: "control", project: selectedProject })}
+ className={`px-3 py-1 rounded-md text-[12px] font-medium transition-all ${
+ activeTab === "control"
+ ? "bg-primary/15 text-primary"
+ : "text-muted-foreground hover:text-foreground hover:bg-surface-3"
+ }`}
+ >
+ {t.tabs.control}
+
diff --git a/graph-ui/src/components/ProjectSwitcher.tsx b/graph-ui/src/components/ProjectSwitcher.tsx
new file mode 100644
index 000000000..b61773faf
--- /dev/null
+++ b/graph-ui/src/components/ProjectSwitcher.tsx
@@ -0,0 +1,106 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { callTool } from "../api/rpc";
+import type { Project } from "../lib/types";
+
+/* The project is the context every other view depends on, so it lives
+ * first in the header as a switcher — not as a trailing chip gating a row
+ * of dead tabs. */
+export function ProjectSwitcher({
+ selected,
+ allProjectsLabel,
+ onSelect,
+ onAllProjects,
+}: {
+ selected: string | null;
+ allProjectsLabel: string;
+ onSelect: (project: string) => void;
+ onAllProjects: () => void;
+}) {
+ const [open, setOpen] = useState(false);
+ const [projects, setProjects] = useState
(null);
+ const rootRef = useRef(null);
+
+ const load = useCallback(() => {
+ if (projects !== null) return;
+ callTool<{ projects: Project[] }>("list_projects", {})
+ .then((result) => setProjects(result.projects ?? []))
+ .catch(() => setProjects([]));
+ }, [projects]);
+
+ useEffect(() => {
+ if (!open) return;
+ const onDocClick = (event: MouseEvent) => {
+ if (rootRef.current && !rootRef.current.contains(event.target as Node))
+ setOpen(false);
+ };
+ const onKey = (event: KeyboardEvent) => {
+ if (event.key === "Escape") setOpen(false);
+ };
+ document.addEventListener("mousedown", onDocClick);
+ document.addEventListener("keydown", onKey);
+ return () => {
+ document.removeEventListener("mousedown", onDocClick);
+ document.removeEventListener("keydown", onKey);
+ };
+ }, [open]);
+
+ return (
+
+
{
+ setOpen((v) => !v);
+ load();
+ }}
+ className={`flex items-center gap-1.5 px-2.5 py-1 rounded-md border text-[13px] transition-all max-w-[280px] ${
+ selected
+ ? "bg-popover border-border/40 text-foreground/85 hover:border-border"
+ : "bg-primary/15 border-primary/40 text-primary hover:bg-primary/25"
+ }`}
+ title={selected ?? "Choose which indexed project to explore"}
+ >
+ {selected ?? "Select a project…"}
+ ▾
+
+ {open && (
+
+ {projects === null && (
+
Loading…
+ )}
+ {projects !== null && projects.length === 0 && (
+
+ No indexed projects yet.
+
+ )}
+ {(projects ?? []).map((project) => (
+
{
+ setOpen(false);
+ onSelect(project.name);
+ }}
+ className={`flex items-center gap-2 w-full text-left px-3 py-1.5 transition-colors hover:bg-surface-3 ${
+ project.name === selected ? "text-primary" : "text-foreground/70"
+ }`}
+ >
+ {project.name}
+ {project.name === selected && (
+ ●
+ )}
+
+ ))}
+
+ {
+ setOpen(false);
+ onAllProjects();
+ }}
+ className="w-full text-left px-3 py-1.5 text-[13px] text-foreground/55 hover:text-foreground/85 hover:bg-surface-3 transition-colors"
+ >
+ {allProjectsLabel} — manage & index →
+
+
+
+ )}
+
+ );
+}