Add plugin framework and settings UI

This commit is contained in:
Dotta
2026-03-13 16:22:34 -05:00
parent 7e288d20fc
commit 80cdbdbd47
103 changed files with 31760 additions and 35 deletions

View File

@@ -29,6 +29,37 @@ import { cn } from "../lib/utils";
import { NotFoundPage } from "../pages/NotFound";
import { Button } from "@/components/ui/button";
const INSTANCE_SETTINGS_MEMORY_KEY = "paperclip.lastInstanceSettingsPath";
const DEFAULT_INSTANCE_SETTINGS_PATH = "/instance/settings/heartbeats";
function normalizeRememberedInstanceSettingsPath(rawPath: string | null): string {
if (!rawPath) return DEFAULT_INSTANCE_SETTINGS_PATH;
const match = rawPath.match(/^([^?#]*)(\?[^#]*)?(#.*)?$/);
const pathname = match?.[1] ?? rawPath;
const search = match?.[2] ?? "";
const hash = match?.[3] ?? "";
if (pathname === "/instance/settings/heartbeats" || pathname === "/instance/settings/plugins") {
return `${pathname}${search}${hash}`;
}
if (/^\/instance\/settings\/plugins\/[^/?#]+$/.test(pathname)) {
return `${pathname}${search}${hash}`;
}
return DEFAULT_INSTANCE_SETTINGS_PATH;
}
function readRememberedInstanceSettingsPath(): string {
if (typeof window === "undefined") return DEFAULT_INSTANCE_SETTINGS_PATH;
try {
return normalizeRememberedInstanceSettingsPath(window.localStorage.getItem(INSTANCE_SETTINGS_MEMORY_KEY));
} catch {
return DEFAULT_INSTANCE_SETTINGS_PATH;
}
}
export function Layout() {
const { sidebarOpen, setSidebarOpen, toggleSidebar, isMobile } = useSidebar();
const { openNewIssue, openOnboarding } = useDialog();
@@ -49,6 +80,7 @@ export function Layout() {
const onboardingTriggered = useRef(false);
const lastMainScrollTop = useRef(0);
const [mobileNavVisible, setMobileNavVisible] = useState(true);
const [instanceSettingsTarget, setInstanceSettingsTarget] = useState<string>(() => readRememberedInstanceSettingsPath());
const nextTheme = theme === "dark" ? "light" : "dark";
const matchedCompany = useMemo(() => {
if (!companyPrefix) return null;
@@ -220,6 +252,21 @@ export function Layout() {
};
}, [isMobile]);
useEffect(() => {
if (!location.pathname.startsWith("/instance/settings/")) return;
const nextPath = normalizeRememberedInstanceSettingsPath(
`${location.pathname}${location.search}${location.hash}`,
);
setInstanceSettingsTarget(nextPath);
try {
window.localStorage.setItem(INSTANCE_SETTINGS_MEMORY_KEY, nextPath);
} catch {
// Ignore storage failures in restricted environments.
}
}, [location.hash, location.pathname, location.search]);
return (
<div
className={cn(
@@ -235,7 +282,6 @@ export function Layout() {
</a>
<WorktreeBanner />
<div className={cn("min-h-0 flex-1", isMobile ? "w-full" : "flex overflow-hidden")}>
{/* Mobile backdrop */}
{isMobile && sidebarOpen && (
<button
type="button"
@@ -245,7 +291,6 @@ export function Layout() {
/>
)}
{/* Combined sidebar area: company rail + inner sidebar + docs bar */}
{isMobile ? (
<div
className={cn(
@@ -270,7 +315,7 @@ export function Layout() {
</a>
<Button variant="ghost" size="icon-sm" className="text-muted-foreground shrink-0" asChild>
<Link
to="/instance/settings"
to={instanceSettingsTarget}
aria-label="Instance settings"
title="Instance settings"
onClick={() => {
@@ -320,7 +365,7 @@ export function Layout() {
</a>
<Button variant="ghost" size="icon-sm" className="text-muted-foreground shrink-0" asChild>
<Link
to="/instance/settings"
to={instanceSettingsTarget}
aria-label="Instance settings"
title="Instance settings"
onClick={() => {
@@ -346,7 +391,6 @@ export function Layout() {
</div>
)}
{/* Main content */}
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "h-full flex-1")}>
<div
className={cn(