feat: refactor UI into sidebar layout (#1318)
* feat(dashboard): refactor UI into Cursor-style sidebar layout
Replace the top-bar AppHeader with a left sidebar that holds the four
primary sections (My Settings, Cloud Agents, Open SWE Review,
Integrations) and the user account menu at the bottom. Admin remains a
hidden route reachable from the sidebar only when is_admin.
UI:
- AppShell + AppSidebar with bottom-anchored avatar/sign-out menu.
- My Settings: profile (first/last name) + PR destination preference.
- Cloud Agents: model/effort, default repo, base branch, branch prefix,
PR toggles (auto-fix CI, create PRs, allow artifacts), Slack
notifications.
- Open SWE Review: trigger mode, draft reviews, PR summaries, autofix
mode + severity threshold, plus the existing per-repo review-style
prompts (was /review-styles).
- Integrations: GitHub install status + Slack/Linear status rows.
- New Switch and Menu primitives.
Backend:
- Extend ProfileUpdate with first_name, last_name, base_branch,
branch_prefix, auto_fix_ci, create_prs, allow_artifacts,
slack_notifications, preferred_pr_destination.
- Add team_settings module + /team-settings GET (any session) / PUT
(admin only) for the reviewer configuration.
* fix(dashboard): replace base-ui Menu with plain dropdown, stabilise Selects
- base-ui's Menu hit an "Invalid hook call / Cannot read properties of
null (reading 'useRef')" crash inside fastComponent under Vite's dep
optimisation. The sidebar account menu is the only consumer, so swap
it for a useState-driven dropdown with click-outside + Esc handling
and drop the unused Menu wrapper.
- Initialise the Open SWE Review settings form with the same defaults
the server returns instead of `null`, so the Selects don't switch
from uncontrolled (`undefined`) to controlled on first data load.
* fix(dashboard): address review feedback on admin overwrite + form reset
- admin PUT /admin/profiles/{login} only sent model/effort/repo from the
admin form. ProfileUpdate's Pydantic defaults then wrote auto_fix_ci /
create_prs / slack_notifications etc back onto the target user's
profile, clobbering whatever they had configured. Switch to
model_dump(exclude_unset=True) and overlay the incoming fields on the
user's existing stored profile so only sent fields change.
- my-settings.tsx and cloud-agents.tsx initialised local state from
profile.data on every change. Because useSaveProfile updates the
cached profile on success, toggling any control would overwrite the
user's typed-but-unsaved input in another field. Guard the
initialisation effect with a ref so it runs once on first load.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-05-20 14:13:23 -07:00
|
|
|
import { Link, useNavigate } from "@tanstack/react-router";
|
|
|
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
|
|
|
import {
|
2026-05-21 09:55:32 -07:00
|
|
|
IoCloudOutline,
|
|
|
|
|
IoExtensionPuzzleOutline,
|
|
|
|
|
IoGitPullRequestOutline,
|
|
|
|
|
IoLogOutOutline,
|
|
|
|
|
IoOptionsOutline,
|
|
|
|
|
IoSettingsOutline,
|
|
|
|
|
} from "react-icons/io5";
|
feat: refactor UI into sidebar layout (#1318)
* feat(dashboard): refactor UI into Cursor-style sidebar layout
Replace the top-bar AppHeader with a left sidebar that holds the four
primary sections (My Settings, Cloud Agents, Open SWE Review,
Integrations) and the user account menu at the bottom. Admin remains a
hidden route reachable from the sidebar only when is_admin.
UI:
- AppShell + AppSidebar with bottom-anchored avatar/sign-out menu.
- My Settings: profile (first/last name) + PR destination preference.
- Cloud Agents: model/effort, default repo, base branch, branch prefix,
PR toggles (auto-fix CI, create PRs, allow artifacts), Slack
notifications.
- Open SWE Review: trigger mode, draft reviews, PR summaries, autofix
mode + severity threshold, plus the existing per-repo review-style
prompts (was /review-styles).
- Integrations: GitHub install status + Slack/Linear status rows.
- New Switch and Menu primitives.
Backend:
- Extend ProfileUpdate with first_name, last_name, base_branch,
branch_prefix, auto_fix_ci, create_prs, allow_artifacts,
slack_notifications, preferred_pr_destination.
- Add team_settings module + /team-settings GET (any session) / PUT
(admin only) for the reviewer configuration.
* fix(dashboard): replace base-ui Menu with plain dropdown, stabilise Selects
- base-ui's Menu hit an "Invalid hook call / Cannot read properties of
null (reading 'useRef')" crash inside fastComponent under Vite's dep
optimisation. The sidebar account menu is the only consumer, so swap
it for a useState-driven dropdown with click-outside + Esc handling
and drop the unused Menu wrapper.
- Initialise the Open SWE Review settings form with the same defaults
the server returns instead of `null`, so the Selects don't switch
from uncontrolled (`undefined`) to controlled on first data load.
* fix(dashboard): address review feedback on admin overwrite + form reset
- admin PUT /admin/profiles/{login} only sent model/effort/repo from the
admin form. ProfileUpdate's Pydantic defaults then wrote auto_fix_ci /
create_prs / slack_notifications etc back onto the target user's
profile, clobbering whatever they had configured. Switch to
model_dump(exclude_unset=True) and overlay the incoming fields on the
user's existing stored profile so only sent fields change.
- my-settings.tsx and cloud-agents.tsx initialised local state from
profile.data on every change. Because useSaveProfile updates the
cached profile on success, toggling any control would overwrite the
user's typed-but-unsaved input in another field. Guard the
initialisation effect with a ref so it runs once on first load.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-05-20 14:13:23 -07:00
|
|
|
import { useEffect, useRef, useState } from "react";
|
2026-05-21 09:55:32 -07:00
|
|
|
import type {ComponentType, SVGProps} from "react";
|
feat: refactor UI into sidebar layout (#1318)
* feat(dashboard): refactor UI into Cursor-style sidebar layout
Replace the top-bar AppHeader with a left sidebar that holds the four
primary sections (My Settings, Cloud Agents, Open SWE Review,
Integrations) and the user account menu at the bottom. Admin remains a
hidden route reachable from the sidebar only when is_admin.
UI:
- AppShell + AppSidebar with bottom-anchored avatar/sign-out menu.
- My Settings: profile (first/last name) + PR destination preference.
- Cloud Agents: model/effort, default repo, base branch, branch prefix,
PR toggles (auto-fix CI, create PRs, allow artifacts), Slack
notifications.
- Open SWE Review: trigger mode, draft reviews, PR summaries, autofix
mode + severity threshold, plus the existing per-repo review-style
prompts (was /review-styles).
- Integrations: GitHub install status + Slack/Linear status rows.
- New Switch and Menu primitives.
Backend:
- Extend ProfileUpdate with first_name, last_name, base_branch,
branch_prefix, auto_fix_ci, create_prs, allow_artifacts,
slack_notifications, preferred_pr_destination.
- Add team_settings module + /team-settings GET (any session) / PUT
(admin only) for the reviewer configuration.
* fix(dashboard): replace base-ui Menu with plain dropdown, stabilise Selects
- base-ui's Menu hit an "Invalid hook call / Cannot read properties of
null (reading 'useRef')" crash inside fastComponent under Vite's dep
optimisation. The sidebar account menu is the only consumer, so swap
it for a useState-driven dropdown with click-outside + Esc handling
and drop the unused Menu wrapper.
- Initialise the Open SWE Review settings form with the same defaults
the server returns instead of `null`, so the Selects don't switch
from uncontrolled (`undefined`) to controlled on first data load.
* fix(dashboard): address review feedback on admin overwrite + form reset
- admin PUT /admin/profiles/{login} only sent model/effort/repo from the
admin form. ProfileUpdate's Pydantic defaults then wrote auto_fix_ci /
create_prs / slack_notifications etc back onto the target user's
profile, clobbering whatever they had configured. Switch to
model_dump(exclude_unset=True) and overlay the incoming fields on the
user's existing stored profile so only sent fields change.
- my-settings.tsx and cloud-agents.tsx initialised local state from
profile.data on every change. Because useSaveProfile updates the
cached profile on success, toggling any control would overwrite the
user's typed-but-unsaved input in another field. Guard the
initialisation effect with a ref so it runs once on first load.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-05-20 14:13:23 -07:00
|
|
|
|
|
|
|
|
import type { SessionUser } from "@/lib/api";
|
|
|
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
|
|
|
import { api } from "@/lib/api";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
|
2026-05-21 09:55:32 -07:00
|
|
|
type IconType = ComponentType<SVGProps<SVGSVGElement>>;
|
feat: refactor UI into sidebar layout (#1318)
* feat(dashboard): refactor UI into Cursor-style sidebar layout
Replace the top-bar AppHeader with a left sidebar that holds the four
primary sections (My Settings, Cloud Agents, Open SWE Review,
Integrations) and the user account menu at the bottom. Admin remains a
hidden route reachable from the sidebar only when is_admin.
UI:
- AppShell + AppSidebar with bottom-anchored avatar/sign-out menu.
- My Settings: profile (first/last name) + PR destination preference.
- Cloud Agents: model/effort, default repo, base branch, branch prefix,
PR toggles (auto-fix CI, create PRs, allow artifacts), Slack
notifications.
- Open SWE Review: trigger mode, draft reviews, PR summaries, autofix
mode + severity threshold, plus the existing per-repo review-style
prompts (was /review-styles).
- Integrations: GitHub install status + Slack/Linear status rows.
- New Switch and Menu primitives.
Backend:
- Extend ProfileUpdate with first_name, last_name, base_branch,
branch_prefix, auto_fix_ci, create_prs, allow_artifacts,
slack_notifications, preferred_pr_destination.
- Add team_settings module + /team-settings GET (any session) / PUT
(admin only) for the reviewer configuration.
* fix(dashboard): replace base-ui Menu with plain dropdown, stabilise Selects
- base-ui's Menu hit an "Invalid hook call / Cannot read properties of
null (reading 'useRef')" crash inside fastComponent under Vite's dep
optimisation. The sidebar account menu is the only consumer, so swap
it for a useState-driven dropdown with click-outside + Esc handling
and drop the unused Menu wrapper.
- Initialise the Open SWE Review settings form with the same defaults
the server returns instead of `null`, so the Selects don't switch
from uncontrolled (`undefined`) to controlled on first data load.
* fix(dashboard): address review feedback on admin overwrite + form reset
- admin PUT /admin/profiles/{login} only sent model/effort/repo from the
admin form. ProfileUpdate's Pydantic defaults then wrote auto_fix_ci /
create_prs / slack_notifications etc back onto the target user's
profile, clobbering whatever they had configured. Switch to
model_dump(exclude_unset=True) and overlay the incoming fields on the
user's existing stored profile so only sent fields change.
- my-settings.tsx and cloud-agents.tsx initialised local state from
profile.data on every change. Because useSaveProfile updates the
cached profile on success, toggling any control would overwrite the
user's typed-but-unsaved input in another field. Guard the
initialisation effect with a ref so it runs once on first load.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-05-20 14:13:23 -07:00
|
|
|
|
|
|
|
|
interface NavItem {
|
|
|
|
|
to: string;
|
|
|
|
|
label: string;
|
|
|
|
|
icon: IconType;
|
|
|
|
|
adminOnly?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const NAV: Array<NavItem> = [
|
2026-05-21 09:55:32 -07:00
|
|
|
{ to: "/my-settings", label: "My Settings", icon: IoOptionsOutline },
|
|
|
|
|
{ to: "/cloud-agents", label: "Cloud Agents", icon: IoCloudOutline },
|
|
|
|
|
{ to: "/review", label: "Open SWE Review", icon: IoGitPullRequestOutline },
|
|
|
|
|
{ to: "/integrations", label: "Integrations", icon: IoExtensionPuzzleOutline },
|
|
|
|
|
{ to: "/admin", label: "Admin", icon: IoSettingsOutline, adminOnly: true },
|
feat: refactor UI into sidebar layout (#1318)
* feat(dashboard): refactor UI into Cursor-style sidebar layout
Replace the top-bar AppHeader with a left sidebar that holds the four
primary sections (My Settings, Cloud Agents, Open SWE Review,
Integrations) and the user account menu at the bottom. Admin remains a
hidden route reachable from the sidebar only when is_admin.
UI:
- AppShell + AppSidebar with bottom-anchored avatar/sign-out menu.
- My Settings: profile (first/last name) + PR destination preference.
- Cloud Agents: model/effort, default repo, base branch, branch prefix,
PR toggles (auto-fix CI, create PRs, allow artifacts), Slack
notifications.
- Open SWE Review: trigger mode, draft reviews, PR summaries, autofix
mode + severity threshold, plus the existing per-repo review-style
prompts (was /review-styles).
- Integrations: GitHub install status + Slack/Linear status rows.
- New Switch and Menu primitives.
Backend:
- Extend ProfileUpdate with first_name, last_name, base_branch,
branch_prefix, auto_fix_ci, create_prs, allow_artifacts,
slack_notifications, preferred_pr_destination.
- Add team_settings module + /team-settings GET (any session) / PUT
(admin only) for the reviewer configuration.
* fix(dashboard): replace base-ui Menu with plain dropdown, stabilise Selects
- base-ui's Menu hit an "Invalid hook call / Cannot read properties of
null (reading 'useRef')" crash inside fastComponent under Vite's dep
optimisation. The sidebar account menu is the only consumer, so swap
it for a useState-driven dropdown with click-outside + Esc handling
and drop the unused Menu wrapper.
- Initialise the Open SWE Review settings form with the same defaults
the server returns instead of `null`, so the Selects don't switch
from uncontrolled (`undefined`) to controlled on first data load.
* fix(dashboard): address review feedback on admin overwrite + form reset
- admin PUT /admin/profiles/{login} only sent model/effort/repo from the
admin form. ProfileUpdate's Pydantic defaults then wrote auto_fix_ci /
create_prs / slack_notifications etc back onto the target user's
profile, clobbering whatever they had configured. Switch to
model_dump(exclude_unset=True) and overlay the incoming fields on the
user's existing stored profile so only sent fields change.
- my-settings.tsx and cloud-agents.tsx initialised local state from
profile.data on every change. Because useSaveProfile updates the
cached profile on success, toggling any control would overwrite the
user's typed-but-unsaved input in another field. Guard the
initialisation effect with a ref so it runs once on first load.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-05-20 14:13:23 -07:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export function AppSidebar({ user }: { user: SessionUser }) {
|
|
|
|
|
const qc = useQueryClient();
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
|
|
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!menuOpen) return;
|
|
|
|
|
const onClickOutside = (e: MouseEvent) => {
|
|
|
|
|
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
|
|
|
setMenuOpen(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const onKey = (e: KeyboardEvent) => {
|
|
|
|
|
if (e.key === "Escape") setMenuOpen(false);
|
|
|
|
|
};
|
|
|
|
|
document.addEventListener("mousedown", onClickOutside);
|
|
|
|
|
document.addEventListener("keydown", onKey);
|
|
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener("mousedown", onClickOutside);
|
|
|
|
|
document.removeEventListener("keydown", onKey);
|
|
|
|
|
};
|
|
|
|
|
}, [menuOpen]);
|
|
|
|
|
|
|
|
|
|
const onLogout = async () => {
|
|
|
|
|
setMenuOpen(false);
|
|
|
|
|
await api.logout();
|
|
|
|
|
qc.setQueryData(["session"], null);
|
|
|
|
|
navigate({ to: "/login" });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const initials = (user.login || "?").slice(0, 2).toUpperCase();
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<aside className="flex h-svh w-60 shrink-0 flex-col border-r border-border bg-sidebar text-sidebar-foreground">
|
|
|
|
|
<div className="px-4 pt-5 pb-4">
|
feat: restructure Open SWE Review tab + wire create_prs (#1319)
* feat(dashboard): restructure Open SWE Review tab + wire create_prs
Restructures the dashboard around two related changes the reviewer settings
have been asking for:
- Wire profile.create_prs. Defaults to true (opt-out); when off the system
prompt gets a `Pull Request Policy Override` section telling the agent
to push the branch and notify with the branch URL instead of opening a
PR. Removes the noop Slack Notifications / Allow Artifacts / First Name
/ Last Name controls and their schema fields.
- Repositories opt-in for Open SWE Review. New per-team enabled list
stored in the LangGraph Store (`["enabled_review_repos"]`). Every
reviewer webhook chokepoint now goes through `_is_repo_enabled_for_review`
which AND-combines the existing env allowlist with the dashboard list.
Default is empty (opt-in) — admins enable repos per-installation from
the new Repositories page nested under Open SWE Review.
- Open SWE Review tab now mirrors the Cursor "rules" pattern: main page
shows installation rows + a Rules entry; both drill into nested pages
(/review/repositories/$owner and /review/styles) with a back link.
- Adds the new logo/favicon assets shipped from sidebar + html head.
Tests pass with a new autouse fixture (`tests/conftest.py`) that defaults
`is_review_repo_enabled` to True for existing allowlist tests.
* fix(dashboard): make main content scroll independently of the sidebar
Outer flex container was min-h-svh, so it grew with main's content and the
whole page scrolled — sidebar moved with it. Pin to h-svh + overflow-hidden
so the sidebar stays put and only <main> scrolls.
* fix(dashboard): make disabled repo toggles obviously disabled
Switch's disabled state used opacity-50 against a muted background, so
the not-admin state looked nearly identical to the off state. Bump to
opacity-40 + grayscale, and wrap each repo toggle in a span carrying a
native hover tooltip explaining why it's disabled.
* fix(switch): handle base-ui's data-disabled state
base-ui's Switch.Root sets data-disabled (not the HTML disabled attribute)
when disabled, so Tailwind's disabled: variant never matches and the
button keeps its cursor-pointer + clickable look. Mirror the styling
under the data-[disabled] variant and add pointer-events-none so the
disabled state is both visible and actually unclickable.
* feat(dashboard): paginate per-installation repository list
20 repos per page with Prev / page X of Y / Next controls at the bottom.
Pager only renders when there are more than 20 repos. Page resets to 0
when navigating between installations.
* feat(dashboard): global default model selectors for Agent + Reviewer
Adds team-wide default model + reasoning effort for both agents in the
Admin tab so operators can switch models without redeploying.
Resolution chain:
Agent: hardcoded -> LLM_MODEL_ID env -> team default -> user profile
Reviewer: hardcoded -> LLM_MODEL_ID env -> team default -> per-call configurable
Team defaults live in team_settings and are validated against the
SUPPORTED_MODELS allowlist + the model's supported reasoning efforts.
'Inherit from env' clears the override and falls back to LLM_MODEL_ID.
* refactor(models): drop LLM_MODEL_ID env in favour of the team default
The team default is now the single source of truth for the runtime model
choice; per-user (agent) and per-call configurable (reviewer) selections
still win on top. When no admin has touched the team default, it surfaces
the hardcoded fallback (DEFAULT_MODEL_ID + its default effort), so the
admin UI's dropdown is always pre-populated with a sensible value.
The Admin UI loses the 'Inherit from env' option since there is no longer
an env layer to inherit from.
* chore(models): set hardcoded fallback to gpt-5.5 medium
Decouple the team-default boot value (gpt-5.5 / medium) from each model's
ProfileForm-suggested default_effort so we can change one without nudging
the other. The Opus xhigh default for new user profiles is unchanged.
* feat(dashboard): trigger-mode copy, Coming Soon badges, logout in My Settings
- Rename trigger mode 'ready_for_review' -> 'once_per_pr' with new
description copy that matches the screenshot. Legacy stored values
fall back to 'every_push' on read so the UI never shows an unknown
selection.
- Add a 'Coming soon' badge + greyed-out + disabled state on the
controls that don't have runtime consumers yet: Trigger Mode,
Autofix Mode, Autofix Severity Threshold, and Automatically fix CI
failures. SettingsRow grew a comingSoon prop to keep this consistent.
- My Settings drops the noop PR Preferences section and adds a Sign
Out button. preferred_pr_destination is removed from the profile
schema; old records get the field popped on next write.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-05-21 09:17:07 -07:00
|
|
|
<Link
|
|
|
|
|
to="/my-settings"
|
|
|
|
|
className="flex items-center gap-2 font-heading text-sm font-medium tracking-tight"
|
|
|
|
|
>
|
|
|
|
|
<img src="/logo-mark.png" alt="" className="size-5" />
|
feat: refactor UI into sidebar layout (#1318)
* feat(dashboard): refactor UI into Cursor-style sidebar layout
Replace the top-bar AppHeader with a left sidebar that holds the four
primary sections (My Settings, Cloud Agents, Open SWE Review,
Integrations) and the user account menu at the bottom. Admin remains a
hidden route reachable from the sidebar only when is_admin.
UI:
- AppShell + AppSidebar with bottom-anchored avatar/sign-out menu.
- My Settings: profile (first/last name) + PR destination preference.
- Cloud Agents: model/effort, default repo, base branch, branch prefix,
PR toggles (auto-fix CI, create PRs, allow artifacts), Slack
notifications.
- Open SWE Review: trigger mode, draft reviews, PR summaries, autofix
mode + severity threshold, plus the existing per-repo review-style
prompts (was /review-styles).
- Integrations: GitHub install status + Slack/Linear status rows.
- New Switch and Menu primitives.
Backend:
- Extend ProfileUpdate with first_name, last_name, base_branch,
branch_prefix, auto_fix_ci, create_prs, allow_artifacts,
slack_notifications, preferred_pr_destination.
- Add team_settings module + /team-settings GET (any session) / PUT
(admin only) for the reviewer configuration.
* fix(dashboard): replace base-ui Menu with plain dropdown, stabilise Selects
- base-ui's Menu hit an "Invalid hook call / Cannot read properties of
null (reading 'useRef')" crash inside fastComponent under Vite's dep
optimisation. The sidebar account menu is the only consumer, so swap
it for a useState-driven dropdown with click-outside + Esc handling
and drop the unused Menu wrapper.
- Initialise the Open SWE Review settings form with the same defaults
the server returns instead of `null`, so the Selects don't switch
from uncontrolled (`undefined`) to controlled on first data load.
* fix(dashboard): address review feedback on admin overwrite + form reset
- admin PUT /admin/profiles/{login} only sent model/effort/repo from the
admin form. ProfileUpdate's Pydantic defaults then wrote auto_fix_ci /
create_prs / slack_notifications etc back onto the target user's
profile, clobbering whatever they had configured. Switch to
model_dump(exclude_unset=True) and overlay the incoming fields on the
user's existing stored profile so only sent fields change.
- my-settings.tsx and cloud-agents.tsx initialised local state from
profile.data on every change. Because useSaveProfile updates the
cached profile on success, toggling any control would overwrite the
user's typed-but-unsaved input in another field. Guard the
initialisation effect with a ref so it runs once on first load.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-05-20 14:13:23 -07:00
|
|
|
open-swe
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<nav className="flex flex-1 flex-col gap-0.5 px-2">
|
|
|
|
|
{NAV.filter((n) => !n.adminOnly || user.is_admin).map((item) => {
|
|
|
|
|
const Icon = item.icon;
|
|
|
|
|
return (
|
|
|
|
|
<Link
|
|
|
|
|
key={item.to}
|
|
|
|
|
to={item.to}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex items-center gap-2.5 rounded-md px-2.5 py-1.5 text-xs/relaxed text-muted-foreground transition-colors",
|
|
|
|
|
"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
|
|
|
|
)}
|
|
|
|
|
activeProps={{
|
|
|
|
|
className: "bg-sidebar-accent text-sidebar-accent-foreground font-medium",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon className="size-4" />
|
|
|
|
|
<span>{item.label}</span>
|
|
|
|
|
</Link>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
<div ref={menuRef} className="relative border-t border-border p-2">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
aria-haspopup="menu"
|
|
|
|
|
aria-expanded={menuOpen}
|
|
|
|
|
onClick={() => setMenuOpen((o) => !o)}
|
|
|
|
|
className="flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-left outline-none hover:bg-sidebar-accent"
|
|
|
|
|
>
|
|
|
|
|
<Avatar className="size-7">
|
|
|
|
|
{user.avatar_url && <AvatarImage src={user.avatar_url} alt={user.login} />}
|
|
|
|
|
<AvatarFallback>{initials}</AvatarFallback>
|
|
|
|
|
</Avatar>
|
|
|
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
|
|
|
<span className="truncate text-xs font-medium">{user.login}</span>
|
|
|
|
|
{user.email && (
|
|
|
|
|
<span className="truncate text-[10px] text-muted-foreground">{user.email}</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
{menuOpen && (
|
|
|
|
|
<div
|
|
|
|
|
role="menu"
|
|
|
|
|
className="absolute bottom-full left-2 right-2 mb-2 overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md"
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
role="menuitem"
|
|
|
|
|
onClick={() => void onLogout()}
|
|
|
|
|
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-xs/relaxed hover:bg-muted"
|
|
|
|
|
>
|
2026-05-21 09:55:32 -07:00
|
|
|
<IoLogOutOutline className="size-3.5" />
|
feat: refactor UI into sidebar layout (#1318)
* feat(dashboard): refactor UI into Cursor-style sidebar layout
Replace the top-bar AppHeader with a left sidebar that holds the four
primary sections (My Settings, Cloud Agents, Open SWE Review,
Integrations) and the user account menu at the bottom. Admin remains a
hidden route reachable from the sidebar only when is_admin.
UI:
- AppShell + AppSidebar with bottom-anchored avatar/sign-out menu.
- My Settings: profile (first/last name) + PR destination preference.
- Cloud Agents: model/effort, default repo, base branch, branch prefix,
PR toggles (auto-fix CI, create PRs, allow artifacts), Slack
notifications.
- Open SWE Review: trigger mode, draft reviews, PR summaries, autofix
mode + severity threshold, plus the existing per-repo review-style
prompts (was /review-styles).
- Integrations: GitHub install status + Slack/Linear status rows.
- New Switch and Menu primitives.
Backend:
- Extend ProfileUpdate with first_name, last_name, base_branch,
branch_prefix, auto_fix_ci, create_prs, allow_artifacts,
slack_notifications, preferred_pr_destination.
- Add team_settings module + /team-settings GET (any session) / PUT
(admin only) for the reviewer configuration.
* fix(dashboard): replace base-ui Menu with plain dropdown, stabilise Selects
- base-ui's Menu hit an "Invalid hook call / Cannot read properties of
null (reading 'useRef')" crash inside fastComponent under Vite's dep
optimisation. The sidebar account menu is the only consumer, so swap
it for a useState-driven dropdown with click-outside + Esc handling
and drop the unused Menu wrapper.
- Initialise the Open SWE Review settings form with the same defaults
the server returns instead of `null`, so the Selects don't switch
from uncontrolled (`undefined`) to controlled on first data load.
* fix(dashboard): address review feedback on admin overwrite + form reset
- admin PUT /admin/profiles/{login} only sent model/effort/repo from the
admin form. ProfileUpdate's Pydantic defaults then wrote auto_fix_ci /
create_prs / slack_notifications etc back onto the target user's
profile, clobbering whatever they had configured. Switch to
model_dump(exclude_unset=True) and overlay the incoming fields on the
user's existing stored profile so only sent fields change.
- my-settings.tsx and cloud-agents.tsx initialised local state from
profile.data on every change. Because useSaveProfile updates the
cached profile on success, toggling any control would overwrite the
user's typed-but-unsaved input in another field. Guard the
initialisation effect with a ref so it runs once on first load.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-05-20 14:13:23 -07:00
|
|
|
Sign out
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
);
|
|
|
|
|
}
|