fix: put Sessions back in the middle of nav on desktop and mobile

- Reorder desktop sidebar so Sessions sits between spaces/tools groups:
  Home, Projects, Workspaces, Sessions, Tool Workshop, Config Profiles, Settings.
- Reorder mobile bottom nav so Sessions is the center item:
  Home, Spaces, Sessions, Tools, Settings.
- Workspace cards already display the owning project name; no extra change needed.

Quality gates: npm run typecheck, npm run lint clean,
npm test -- --run 87 passed.
This commit is contained in:
Developer
2026-06-13 19:44:47 +00:00
parent 4ced6141ae
commit 1d09193652
19 changed files with 321 additions and 293 deletions
+146 -146
View File
@@ -16,166 +16,166 @@ import { StartToolFAB } from "./features/tool/start-tool-fab";
import type { IconName } from "../utils/icons";
const NAV_ITEMS: {
to: string;
label: string;
icon: IconName;
badge?: "sessions";
to: string;
label: string;
icon: IconName;
badge?: "sessions";
}[] = [
{ to: "/", label: "Home", icon: "dashboard" },
{ to: "/sessions", label: "Sessions", icon: "terminal", badge: "sessions" },
{ to: "/workspaces", label: "Workspaces", icon: "folder" },
{ to: "/projects", label: "Projects", icon: "projects" },
{ to: "/tool-workshop", label: "Tool Workshop", icon: "settings" },
{ to: "/config-profiles", label: "Config Profiles", icon: "folder" },
{ to: "/settings", label: "Settings", icon: "settings" },
{ to: "/", label: "Home", icon: "dashboard" },
{ to: "/projects", label: "Projects", icon: "projects" },
{ to: "/workspaces", label: "Workspaces", icon: "folder" },
{ to: "/sessions", label: "Sessions", icon: "terminal", badge: "sessions" },
{ to: "/tool-workshop", label: "Tool Workshop", icon: "settings" },
{ to: "/config-profiles", label: "Config Profiles", icon: "folder" },
{ to: "/settings", label: "Settings", icon: "settings" },
];
const SessionItem = ({ session }: { session: Session }) => {
const isRunning = session.status === "running";
const isRunning = session.status === "running";
// Determine the link target:
// - Web tools open their tunnel URL
// - Terminal tools open the terminal page
// - Everything else falls back to the project page
const hasTerminal = session.tool_type_interfaces.includes("terminal");
const hasWeb = session.tool_type_interfaces.includes("web");
const href =
session.url && hasWeb
? session.url
: hasTerminal
? `/instances/${session.id}/terminal`
: `/projects/${session.project_id}`;
// Determine the link target:
// - Web tools open their tunnel URL
// - Terminal tools open the terminal page
// - Everything else falls back to the project page
const hasTerminal = session.tool_type_interfaces.includes("terminal");
const hasWeb = session.tool_type_interfaces.includes("web");
const href =
session.url && hasWeb
? session.url
: hasTerminal
? `/instances/${session.id}/terminal`
: `/projects/${session.project_id}`;
const tooltipParts = [session.display_name, session.project_name];
if (session.workspace_name) tooltipParts.push(session.workspace_name);
else if (session.repository_name) tooltipParts.push(session.repository_name);
tooltipParts.push(`(${session.status})`);
const tooltipParts = [session.display_name, session.project_name];
if (session.workspace_name) tooltipParts.push(session.workspace_name);
else if (session.repository_name) tooltipParts.push(session.repository_name);
tooltipParts.push(`(${session.status})`);
return (
<a
href={href}
target={`session-${session.id}`}
rel="noreferrer"
className="nav-item session-item"
title={tooltipParts.join(" ")}
>
<span className={`session-status ${isRunning ? "running" : ""}`} />
<Icon name={session.tool_icon as IconName} size="sm" />
<span className="session-name">
{session.display_name}
<span className="session-tool">{session.tool_type_name}</span>
</span>
</a>
);
return (
<a
href={href}
target={`session-${session.id}`}
rel="noreferrer"
className="nav-item session-item"
title={tooltipParts.join(" ")}
>
<span className={`session-status ${isRunning ? "running" : ""}`} />
<Icon name={session.tool_icon as IconName} size="sm" />
<span className="session-name">
{session.display_name}
<span className="session-tool">{session.tool_type_name}</span>
</span>
</a>
);
};
export const AppShell = () => {
useTheme();
const { user, logout } = useAuth();
const { sessions } = useSessions();
const location = useLocation();
const isMobile = useMobileViewport();
const isMobileTerminal =
isMobile &&
location.pathname.includes("/instances/") &&
location.pathname.includes("/terminal");
useTheme();
const { user, logout } = useAuth();
const { sessions } = useSessions();
const location = useLocation();
const isMobile = useMobileViewport();
const isMobileTerminal =
isMobile &&
location.pathname.includes("/instances/") &&
location.pathname.includes("/terminal");
if (isMobileTerminal) {
return (
<EventProvider>
<ToastProvider>
<NotificationProvider>
<EventToastBridge />
<div className="shell mobile-terminal-shell">
<Outlet />
</div>
</NotificationProvider>
</ToastProvider>
</EventProvider>
);
}
if (isMobileTerminal) {
return (
<EventProvider>
<ToastProvider>
<NotificationProvider>
<EventToastBridge />
<div className="shell mobile-terminal-shell">
<Outlet />
</div>
</NotificationProvider>
</ToastProvider>
</EventProvider>
);
}
return (
<EventProvider>
<ToastProvider>
<NotificationProvider>
<EventToastBridge />
<div className="shell">
<header className="shell-header">
<Link className="brand" to="/">
Headquarter
</Link>
<div className="header-actions">
<NotificationCenter isMobileTerminal={isMobileTerminal} />
<Link className="user-chip" to="/profile">
{user?.name ?? "User"}
</Link>
<button
className="ghost-button"
onClick={() => {
void logout();
}}
type="button"
>
<Icon name="logout" size="sm" />
Logout
</button>
</div>
</header>
return (
<EventProvider>
<ToastProvider>
<NotificationProvider>
<EventToastBridge />
<div className="shell">
<header className="shell-header">
<Link className="brand" to="/">
Headquarter
</Link>
<div className="header-actions">
<NotificationCenter isMobileTerminal={isMobileTerminal} />
<Link className="user-chip" to="/profile">
{user?.name ?? "User"}
</Link>
<button
className="ghost-button"
onClick={() => {
void logout();
}}
type="button"
>
<Icon name="logout" size="sm" />
Logout
</button>
</div>
</header>
<div className="shell-body">
{!isMobile && (
<aside className="shell-nav" aria-label="Primary navigation">
{NAV_ITEMS.map((item) => {
const activeCount = sessions.filter(
(s) => s.status === "running",
).length;
return (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) =>
isActive ? "nav-item nav-item-active" : "nav-item"
}
end={item.to === "/"}
>
<Icon name={item.icon} size="sm" />
{item.label}
{item.badge === "sessions" && activeCount > 0 && (
<span className="nav-badge">{activeCount}</span>
)}
</NavLink>
);
})}
<div className="shell-body">
{!isMobile && (
<aside className="shell-nav" aria-label="Primary navigation">
{NAV_ITEMS.map((item) => {
const activeCount = sessions.filter(
(s) => s.status === "running",
).length;
return (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) =>
isActive ? "nav-item nav-item-active" : "nav-item"
}
end={item.to === "/"}
>
<Icon name={item.icon} size="sm" />
{item.label}
{item.badge === "sessions" && activeCount > 0 && (
<span className="nav-badge">{activeCount}</span>
)}
</NavLink>
);
})}
{sessions.length > 0 && (
<>
<div className="nav-divider" />
<div className="nav-section-title">Live sessions</div>
{sessions.map((session) => (
<SessionItem key={session.id} session={session} />
))}
</>
)}
</aside>
)}
{sessions.length > 0 && (
<>
<div className="nav-divider" />
<div className="nav-section-title">Live sessions</div>
{sessions.map((session) => (
<SessionItem key={session.id} session={session} />
))}
</>
)}
</aside>
)}
<main className={`shell-content ${isMobile ? "mobile" : ""}`}>
<Outlet />
</main>
</div>
<main className={`shell-content ${isMobile ? "mobile" : ""}`}>
<Outlet />
</main>
</div>
{isMobile && (
<MobileNav
sessionCount={
sessions.filter((s) => s.status === "running").length
}
/>
)}
<StartToolFAB />
</div>
</NotificationProvider>
</ToastProvider>
</EventProvider>
);
{isMobile && (
<MobileNav
sessionCount={
sessions.filter((s) => s.status === "running").length
}
/>
)}
<StartToolFAB />
</div>
</NotificationProvider>
</ToastProvider>
</EventProvider>
);
};