Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9,697 changes: 8,539 additions & 1,158 deletions interface/package-lock.json

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions interface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest run"
},
"dependencies": {
"@codemirror/language": "^6.12.1",
Expand Down Expand Up @@ -60,15 +61,19 @@
"zod": "^4.3.6"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.18",
"jsdom": "^28.1.0",
"postcss": "^8.4.36",
"sass": "^1.72.0",
"tailwindcss": "^3.4.1",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.6.2",
"vite": "^6.0.0"
"vite": "^6.0.0",
"vitest": "^4.0.18"
}
}
45 changes: 45 additions & 0 deletions interface/src/components/AgentTabs.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { AgentTabs } from "./AgentTabs";

vi.mock("@tanstack/react-router", () => ({
useMatchRoute: () => () => false,
Link: ({ children, className }: { children: React.ReactNode; className?: string; to?: string; params?: unknown }) => (
<a className={className}>{children}</a>
),
}));

describe("AgentTabs", () => {
it("renders 10 tab links", () => {
const { container } = render(<AgentTabs agentId="main" />);
const links = container.querySelectorAll("a");
expect(links).toHaveLength(10);
});

it("container has flex and h-12 classes", () => {
const { container } = render(<AgentTabs agentId="main" />);
const div = container.firstElementChild!;
expect(div).toHaveClass("flex");
expect(div).toHaveClass("h-12");
expect(div).toHaveClass("border-b");
});

it("container has overflow-x-auto class", () => {
const { container } = render(<AgentTabs agentId="main" />);
const div = container.firstElementChild!;
expect(div).toHaveClass("overflow-x-auto");
});

it("container has no-scrollbar class", () => {
const { container } = render(<AgentTabs agentId="main" />);
const div = container.firstElementChild!;
expect(div).toHaveClass("no-scrollbar");
});

it("renders all tab labels", () => {
render(<AgentTabs agentId="main" />);
expect(screen.getByText("Overview")).toBeDefined();
expect(screen.getByText("Chat")).toBeDefined();
expect(screen.getByText("Config")).toBeDefined();
});
});
2 changes: 1 addition & 1 deletion interface/src/components/AgentTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function AgentTabs({ agentId }: { agentId: string }) {
const matchRoute = useMatchRoute();

return (
<div className="relative flex h-12 items-stretch border-b border-app-line bg-app-darkBox/30 px-6">
<div className="relative flex h-12 items-stretch overflow-x-auto no-scrollbar border-b border-app-line bg-app-darkBox/30 px-6">
{tabs.map((tab) => {
const isActive = matchRoute({
to: tab.to,
Expand Down
102 changes: 102 additions & 0 deletions interface/src/components/SettingSectionNav.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { describe, it, expect, vi } from "vitest";
import { render } from "@testing-library/react";
import { SettingSectionNav } from "./SettingSectionNav";

const singleGroup = [
{
label: "Settings",
sections: [
{ id: "a", label: "Section A" },
{ id: "b", label: "Section B" },
{ id: "c", label: "Section C" },
],
},
];

const multiGroup = [
{
label: "Group One",
sections: [
{ id: "a", label: "Section A" },
{ id: "b", label: "Section B" },
],
},
{
label: "Group Two",
sections: [
{ id: "c", label: "Section C" },
{ id: "d", label: "Section D" },
{ id: "e", label: "Section E" },
],
},
];

describe("SettingSectionNav", () => {
it("desktop sidebar div has hidden md:flex classes", () => {
const { container } = render(
<SettingSectionNav groups={singleGroup} activeSection="a" onSectionChange={vi.fn()} />,
);
const desktopDiv = container.children[0] as HTMLElement;
expect(desktopDiv).toHaveClass("hidden");
expect(desktopDiv).toHaveClass("md:flex");
});

it("mobile tab bar div has flex md:hidden classes", () => {
const { container } = render(
<SettingSectionNav groups={singleGroup} activeSection="a" onSectionChange={vi.fn()} />,
);
const mobileDiv = container.children[1] as HTMLElement;
expect(mobileDiv).toHaveClass("flex");
expect(mobileDiv).toHaveClass("md:hidden");
});

it("renders correct number of buttons for a single-group config", () => {
const { container } = render(
<SettingSectionNav groups={singleGroup} activeSection="a" onSectionChange={vi.fn()} />,
);
const desktopDiv = container.children[0] as HTMLElement;
const mobileDiv = container.children[1] as HTMLElement;
expect(desktopDiv.querySelectorAll("button")).toHaveLength(3);
expect(mobileDiv.querySelectorAll("button")).toHaveLength(3);
});

it("renders correct number of buttons for a multi-group config", () => {
const { container } = render(
<SettingSectionNav groups={multiGroup} activeSection="a" onSectionChange={vi.fn()} />,
);
const desktopDiv = container.children[0] as HTMLElement;
const mobileDiv = container.children[1] as HTMLElement;
expect(desktopDiv.querySelectorAll("button")).toHaveLength(5);
expect(mobileDiv.querySelectorAll("button")).toHaveLength(5);
});

it("group separator rendered in mobile bar when multiple groups provided", () => {
const { container } = render(
<SettingSectionNav groups={multiGroup} activeSection="a" onSectionChange={vi.fn()} />,
);
const mobileDiv = container.children[1] as HTMLElement;
const separators = mobileDiv.querySelectorAll("div.w-px");
expect(separators).toHaveLength(1);
});

it("no separator rendered in mobile bar for single group", () => {
const { container } = render(
<SettingSectionNav groups={singleGroup} activeSection="a" onSectionChange={vi.fn()} />,
);
const mobileDiv = container.children[1] as HTMLElement;
const separators = mobileDiv.querySelectorAll("div.w-px");
expect(separators).toHaveLength(0);
});

it("active section button has active styling class", () => {
const { container } = render(
<SettingSectionNav groups={singleGroup} activeSection="b" onSectionChange={vi.fn()} />,
);
const desktopDiv = container.children[0] as HTMLElement;
const desktopButtons = desktopDiv.querySelectorAll("button");
expect(desktopButtons[1]).toHaveClass("bg-app-darkBox");
const mobileDiv = container.children[1] as HTMLElement;
const mobileButtons = mobileDiv.querySelectorAll("button");
expect(mobileButtons[1]).toHaveClass("text-ink");
});
});
76 changes: 76 additions & 0 deletions interface/src/components/SettingSectionNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from "react";
import {SettingSidebarButton} from "@/ui";

interface SectionGroup {
label: string;
sections: {
id: string;
label: string;
badge?: React.ReactNode;
}[];
}

interface SettingSectionNavProps {
groups: SectionGroup[];
activeSection: string;
onSectionChange: (id: string) => void;
}

export function SettingSectionNav({groups, activeSection, onSectionChange}: SettingSectionNavProps) {
return (
<>
{/* Desktop sidebar */}
<div className="hidden md:flex w-52 flex-shrink-0 flex-col border-r border-app-line/50 bg-app-darkBox/20 overflow-y-auto">
{groups.map((group) => (
<React.Fragment key={group.label}>
<div className="px-3 pb-1 pt-4">
<span className="text-tiny font-medium uppercase tracking-wider text-ink-faint">
{group.label}
</span>
</div>
<div className="flex flex-col gap-0.5 px-2">
{group.sections.map((section) => (
<SettingSidebarButton
key={section.id}
onClick={() => onSectionChange(section.id)}
active={activeSection === section.id}
>
<span className="flex-1">{section.label}</span>
{section.badge}
</SettingSidebarButton>
))}
</div>
</React.Fragment>
))}
</div>

{/* Mobile tab bar */}
<div className="flex md:hidden h-10 items-stretch overflow-x-auto no-scrollbar border-b border-app-line/50 bg-app-darkBox/20 px-2">
{groups.map((group, groupIndex) => (
<React.Fragment key={group.label}>
{groupIndex > 0 && (
<div className="w-px bg-app-line/30 self-stretch my-1" />
)}
{group.sections.map((section) => {
const isActive = activeSection === section.id;
return (
<button
key={section.id}
onClick={() => onSectionChange(section.id)}
className={`relative whitespace-nowrap px-3 text-sm transition-colors ${
isActive ? "text-ink" : "text-ink-faint hover:text-ink-dull"
}`}
>
{section.label}
{isActive && (
<div className="absolute bottom-0 left-0 right-0 h-px bg-accent" />
)}
</button>
);
})}
</React.Fragment>
))}
</div>
</>
);
}
Loading