feat: AI Router per session; Settings at the sidebar's foot; shipped tool icons
AI Router is a session's choice, not a workspace's: - an AI Router select beside the model picker in the workspace home and a pane's run form (RouterPicker.tsx; hidden off Claude/Codex or with only Native; last pick remembered in agenthub.session.router) - `router` on POST /api/lanes/:id/start and POST /api/run; Claude gets --settings=<data dir>/router-settings/<name>.json (0600, outranks project and user settings, key never in argv), Codex -p <name> + key via tmux -e - dashboard_runs.router; a resume naming none goes back to it - profile PATCH rewrites the session files, DELETE removes them - gone: POST /api/lanes/:id/router/apply, LaneRouterRow, the Add-workspace router radio, the claude-/codex-home `routers` answers; clearLaneRouters takes the old keys out of each workspace's settings.local.json at start Sidebar: Settings pinned to the foot, the collapse toggle beside it (stacked on the rail) instead of in the brand's corner. Icons: shipped SVG marks for claude, codex, openclaw and hermes (a black/white pair swapped by theme) in client/public/ai-tools/, used by AiToolIcon before the server's favicon.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @file Sidebar.tsx
|
||||
* @description The app's left sidebar: the brand with a small collapse/expand button in its corner, the navigation links and the workspace list. It also owns the connection details modal (opened from the status bar), which carries the version and the update check — the sidebar itself has no footer, language or theme controls (those live in Settings → General / Appearance). The collapsed state is kept in localStorage.
|
||||
* @description The app's left sidebar: the brand, the navigation links and the workspace list, with Settings and the collapse/expand button pinned to its foot. It also owns the connection details modal (opened from the status bar), which carries the version and the update check — the sidebar itself has no footer, language or theme controls (those live in Settings → General / Appearance). The collapsed state is kept in localStorage.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
/* =============================================================================
|
||||
@@ -82,7 +82,6 @@ function isUpdatePayload(x: unknown): x is UpdateStatusPayload {
|
||||
const NAV_KEYS = [
|
||||
{ to: "/", icon: LayoutDashboard, key: "nav:dashboard" },
|
||||
{ to: "/sessions", icon: FolderOpen, key: "nav:sessions" },
|
||||
{ to: "/settings", icon: Settings, key: "nav:settings" },
|
||||
] as const;
|
||||
|
||||
// SmartGift wordmark, replacing the old text subtitle. Remote URLs: the logo is
|
||||
@@ -384,32 +383,23 @@ export function Sidebar({
|
||||
onUpdateAvailableChange?.(updateAvailable);
|
||||
}, [updateAvailable, onUpdateAvailableChange]);
|
||||
|
||||
const navClass = ({ isActive }: { isActive: boolean }) =>
|
||||
`flex items-center gap-3 rounded-lg text-sm font-medium transition-colors duration-150 ${
|
||||
collapsed ? "justify-center px-2 py-2.5" : "px-3 py-2.5"
|
||||
} ${
|
||||
isActive
|
||||
? "bg-accent/10 text-accent border border-accent/20"
|
||||
: "text-fg-secondary hover:text-fg-primary hover:bg-surface-3 border border-transparent"
|
||||
}`;
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`fixed left-0 top-0 bottom-0 bg-surface-1 border-r border-border flex flex-col z-30 overflow-hidden transition-[width] duration-200 ${
|
||||
collapsed ? "w-[4.25rem]" : "w-60"
|
||||
}`}
|
||||
>
|
||||
{/* Brand, with the collapse toggle as a small button in its corner (or
|
||||
under the mark on the rail) rather than a full-width row. */}
|
||||
<div className="relative px-3 py-4 border-b border-border flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
data-testid="sidebar-toggle"
|
||||
title={collapsed ? t("nav:expand") : t("nav:collapse")}
|
||||
aria-label={collapsed ? t("nav:expand") : t("nav:collapse")}
|
||||
className={`z-10 inline-flex h-6 w-6 items-center justify-center rounded-md text-fg-muted transition-colors hover:bg-surface-3 hover:text-fg-primary ${
|
||||
collapsed ? "absolute bottom-1 left-1/2 -translate-x-1/2" : "absolute right-1.5 top-1.5"
|
||||
}`}
|
||||
>
|
||||
{collapsed ? (
|
||||
<PanelLeftOpen className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<PanelLeftClose className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
<div className={`flex flex-col items-center justify-center ${collapsed ? "pb-4" : ""}`}>
|
||||
<div className="px-3 py-4 border-b border-border flex-shrink-0">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
{collapsed ? (
|
||||
<img src={BRAND_MARK} alt={t("nav:brandSub")} className="w-8 h-8 flex-shrink-0" />
|
||||
) : (
|
||||
@@ -431,7 +421,7 @@ export function Sidebar({
|
||||
</div>
|
||||
|
||||
{/* Nav - only this section scrolls when its items overflow; the rest of
|
||||
the sidebar (brand, language, collapse toggle, footer) stays pinned.
|
||||
the sidebar (brand, and the Settings + collapse footer) stays pinned.
|
||||
Chevron buttons appear at the edges when content is clipped, so the
|
||||
user knows there's more to reach without inspecting the scrollbar. */}
|
||||
<div className="flex-1 min-h-0 relative flex">
|
||||
@@ -444,15 +434,7 @@ export function Sidebar({
|
||||
to={to}
|
||||
end={to === "/"}
|
||||
title={collapsed ? label : undefined}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-3 rounded-lg text-sm font-medium transition-colors duration-150 ${
|
||||
collapsed ? "justify-center px-2 py-2.5" : "px-3 py-2.5"
|
||||
} ${
|
||||
isActive
|
||||
? "bg-accent/10 text-accent border border-accent/20"
|
||||
: "text-fg-secondary hover:text-fg-primary hover:bg-surface-3 border border-transparent"
|
||||
}`
|
||||
}
|
||||
className={navClass}
|
||||
>
|
||||
<Icon className="w-4 h-4 flex-shrink-0" />
|
||||
{!collapsed && <span>{label}</span>}
|
||||
@@ -499,6 +481,39 @@ export function Sidebar({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Settings and the collapse toggle stick to the foot, out of the
|
||||
scrolling nav. On the rail they stack. */}
|
||||
<div
|
||||
className={`flex flex-shrink-0 gap-1 border-t border-border px-2 py-2 ${
|
||||
collapsed ? "flex-col" : "items-center"
|
||||
}`}
|
||||
>
|
||||
<NavLink
|
||||
to="/settings"
|
||||
title={collapsed ? t("nav:settings") : undefined}
|
||||
className={(p) => `${navClass(p)} ${collapsed ? "" : "flex-1"}`}
|
||||
>
|
||||
<Settings className="w-4 h-4 flex-shrink-0" />
|
||||
{!collapsed && <span>{t("nav:settings")}</span>}
|
||||
</NavLink>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
data-testid="sidebar-toggle"
|
||||
title={collapsed ? t("nav:expand") : t("nav:collapse")}
|
||||
aria-label={collapsed ? t("nav:expand") : t("nav:collapse")}
|
||||
className={`inline-flex items-center justify-center rounded-lg text-fg-muted transition-colors hover:bg-surface-3 hover:text-fg-primary ${
|
||||
collapsed ? "py-2.5" : "h-9 w-9 flex-shrink-0"
|
||||
}`}
|
||||
>
|
||||
{collapsed ? (
|
||||
<PanelLeftOpen className="h-4 w-4" />
|
||||
) : (
|
||||
<PanelLeftClose className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ConnectionStatusModal
|
||||
open={connectionOpen}
|
||||
onClose={() => onConnectionOpenChange(false)}
|
||||
|
||||
Reference in New Issue
Block a user