Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5dea2c9
feat(entities): add list driven ui
casey-brooks Feb 14, 2026
2bb6432
fix(entities): add validation and sorting
casey-brooks Feb 14, 2026
1bf0ad3
fix(entities): show inline duplicate errors
casey-brooks Feb 14, 2026
61b2652
feat(ui): add memory list page
casey-brooks Feb 14, 2026
b1117d4
fix(ui): align memory nav behavior
casey-brooks Feb 14, 2026
aac72e7
refactor(entities): align ui primitives and tests
casey-brooks Feb 14, 2026
21b93c7
refactor(entities-ui): use shared select primitives
casey-brooks Feb 15, 2026
a96e8b0
feat(entities): embed node properties dialogs
casey-brooks Feb 15, 2026
76628a7
fix(entities): align node dialog feedback
casey-brooks Feb 15, 2026
3abd9f2
chore(entities): revert sidebar integration
casey-brooks Feb 15, 2026
ef2a761
chore(entities): remove unused hooks
casey-brooks Feb 15, 2026
58d862d
feat(entities): embed config views and layout
casey-brooks Feb 16, 2026
e7ba598
fix(entities): pass graph context to form dialog
casey-brooks Feb 16, 2026
466d059
style(entities): match secrets layout spec
casey-brooks Feb 16, 2026
6a5b936
chore(ui): remove legacy table component
casey-brooks Feb 16, 2026
b54e26c
feat(ui): align entity layouts with secrets
casey-brooks Feb 16, 2026
0f61041
fix(ui): align memory workspace views
casey-brooks Feb 18, 2026
8a0917d
fix(graph): exclude memory workspaces
casey-brooks Feb 18, 2026
e764e27
fix(workspaces): block memory templates
casey-brooks Feb 18, 2026
92f5a4b
feat(memory): convert memory page to entities
casey-brooks Feb 18, 2026
e82be0f
fix(graph): widen template option set types
casey-brooks Feb 18, 2026
4321230
feat(entities): split mcp management
casey-brooks Feb 18, 2026
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
2 changes: 1 addition & 1 deletion packages/platform-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@
"@radix-ui/react-radio-group": "1.2.3",
"@radix-ui/react-roving-focus": "1.0.4",
"@radix-ui/react-scroll-area": "1.2.3",
"@radix-ui/react-select": "2.1.6",
"@radix-ui/react-separator": "1.1.2",
"@radix-ui/react-slider": "1.2.3",
"@radix-ui/react-slot": "1.1.2",
"@radix-ui/react-switch": "1.1.3",
"@radix-ui/react-select": "2.1.6",
"@radix-ui/react-tabs": "1.1.3",
"@radix-ui/react-toggle": "1.1.2",
"@radix-ui/react-toggle-group": "1.1.2",
Expand Down
19 changes: 16 additions & 3 deletions packages/platform-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ import { SettingsLlm } from './pages/SettingsLlm';
import { AgentsGraphContainer } from './features/graph/containers/AgentsGraphContainer';
import { OnboardingPage } from './pages/OnboardingPage';
import { OnboardingGate } from './features/onboarding/components/OnboardingGate';
import { AgentsListPage } from './pages/AgentsListPage';
import { TriggersListPage } from './pages/TriggersListPage';
import { ToolsListPage } from './pages/ToolsListPage';
import { WorkspacesListPage } from './pages/WorkspacesListPage';
import { MemoryEntitiesListPage } from './pages/MemoryEntitiesListPage';
import { McpServersListPage } from './pages/McpServersListPage';

const queryClient = new QueryClient();

Expand All @@ -29,20 +35,27 @@ function App() {
<RuntimeTemplatesProvider>
<TooltipProvider delayDuration={200}>
<Routes>
<Route path="/" element={<Navigate to="/agents/graph" replace />} />
<Route path="/" element={<Navigate to="/agents" replace />} />
<Route path="/onboarding" element={<OnboardingPage />} />

<Route element={<OnboardingGate />}>
<Route element={<RootLayout />}>
{/* Agents */}
<Route path="/agents" element={<AgentsListPage />} />
<Route path="/agents/graph" element={<AgentsGraphContainer />} />
<Route path="/agents/chat" element={<AgentsChat />} />
<Route path="/agents/threads" element={<AgentsThreads />} />
<Route path="/agents/threads/:threadId" element={<AgentsThreads />} />
<Route path="/agents/threads/:threadId/runs/:runId/timeline" element={<AgentsRunScreen />} />
<Route path="/agents/reminders" element={<AgentsReminders />} />
<Route path="/agents/memory" element={<AgentsMemoryManager />} />
<Route path="/memory/*" element={<Navigate to="/agents/memory" replace />} />

{/* Entities */}
<Route path="/triggers" element={<TriggersListPage />} />
<Route path="/tools" element={<ToolsListPage />} />
<Route path="/mcp" element={<McpServersListPage />} />
<Route path="/workspaces" element={<WorkspacesListPage />} />
<Route path="/memory" element={<MemoryEntitiesListPage />} />

{/* Tracing */}
<Route path="/tracing/traces" element={<TracingTraces />} />
Expand Down Expand Up @@ -70,7 +83,7 @@ function App() {
</Route>
</Route>

<Route path="*" element={<Navigate to="/agents/graph" replace />} />
<Route path="*" element={<Navigate to="/agents" replace />} />
</Routes>
</TooltipProvider>
</RuntimeTemplatesProvider>
Expand Down
72 changes: 39 additions & 33 deletions packages/platform-ui/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,49 @@
import { type ButtonHTMLAttributes, type ReactNode } from 'react';
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react';
import { Slot } from '@radix-ui/react-slot';

import { cn } from '@/lib/utils';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'accent' | 'outline' | 'ghost' | 'danger' | 'link';
size?: 'sm' | 'md' | 'lg';
size?: 'sm' | 'md' | 'lg' | 'icon';
children: ReactNode;
asChild?: boolean;
}

export function Button({
variant = 'primary',
size = 'md',
children,
className = '',
...props
}: ButtonProps) {
const baseStyles = 'inline-flex items-center justify-center rounded-[10px] transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed';

const variants: Record<NonNullable<ButtonProps['variant']>, string> = {
primary: 'bg-[var(--agyn-blue)] text-white hover:bg-[var(--agyn-blue-dark)] active:bg-[var(--agyn-blue-dark)]',
secondary: 'bg-[var(--agyn-purple)] text-white hover:opacity-90 active:opacity-80',
accent: 'bg-[var(--agyn-cyan)] text-white hover:opacity-90 active:opacity-80',
outline: 'bg-transparent border-2 border-[var(--agyn-blue)] text-[var(--agyn-blue)] hover:bg-[var(--agyn-blue)] hover:text-white',
ghost: 'bg-transparent text-[var(--agyn-blue)] hover:bg-[var(--agyn-blue)] hover:text-white',
danger: 'bg-transparent border-2 border-[var(--agyn-status-failed)] text-[var(--agyn-status-failed)] hover:bg-[var(--agyn-status-failed)] hover:text-white',
link: 'bg-transparent text-[var(--agyn-blue)] underline-offset-4 hover:underline focus-visible:underline px-0 py-0',
};

const sizes = {
sm: 'px-4 py-2 text-sm',
md: 'px-6 py-3',
lg: 'px-8 py-4',
};
const sizeClass = variant === 'link' ? 'text-sm font-medium' : sizes[size];
const baseStyles =
'inline-flex items-center justify-center rounded-[10px] font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[var(--agyn-blue)] focus-visible:ring-offset-2';

const variants: Record<NonNullable<ButtonProps['variant']>, string> = {
primary:
'bg-[var(--agyn-blue)] text-white hover:bg-[var(--agyn-blue-dark)] active:bg-[var(--agyn-blue-dark)]',
secondary: 'bg-[var(--agyn-purple)] text-white hover:opacity-90 active:opacity-80',
accent: 'bg-[var(--agyn-cyan)] text-white hover:opacity-90 active:opacity-80',
outline:
'bg-transparent border-2 border-[var(--agyn-blue)] text-[var(--agyn-blue)] hover:bg-[var(--agyn-blue)] hover:text-white',
ghost: 'bg-transparent text-[var(--agyn-blue)] hover:bg-[var(--agyn-blue)] hover:text-white',
danger:
'bg-transparent border-2 border-[var(--agyn-status-failed)] text-[var(--agyn-status-failed)] hover:bg-[var(--agyn-status-failed)] hover:text-white',
link:
'bg-transparent text-[var(--agyn-blue)] underline-offset-4 hover:underline focus-visible:underline px-0 py-0',
};

const sizeClasses: Record<Exclude<ButtonProps['size'], undefined>, string> = {
sm: 'px-4 py-2 text-sm',
md: 'px-6 py-3',
lg: 'px-8 py-4',
icon: 'p-0 h-10 w-10',
};

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
{ variant = 'primary', size = 'md', children, className = '', asChild = false, ...props },
ref,
) {
const Comp = asChild ? Slot : 'button';
const appliedSize = variant === 'link' ? 'text-sm font-medium px-0 py-0' : sizeClasses[size];

return (
<button
className={`${baseStyles} ${variants[variant]} ${sizeClass} ${className}`}
{...props}
>
<Comp ref={ref} className={cn(baseStyles, variants[variant], appliedSize, className)} {...props}>
{children}
</button>
</Comp>
);
}
});
84 changes: 69 additions & 15 deletions packages/platform-ui/src/components/SelectInput.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import { forwardRef, type ReactNode, type SelectHTMLAttributes } from 'react';
import { forwardRef, type ChangeEvent, type ReactNode, type SelectHTMLAttributes } from 'react';
import { cn } from '@/lib/utils';

type SelectOption = {
export type SelectOption = {
value: string;
label: ReactNode;
disabled?: boolean;
};

interface SelectInputProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
export interface SelectGroup {
label?: string;
options: SelectOption[];
}

export interface SelectInputProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
label?: ReactNode;
helperText?: ReactNode;
error?: ReactNode;
placeholder?: string;
options: SelectOption[];
options?: SelectOption[];
groups?: SelectGroup[];
size?: 'sm' | 'default';
allowEmptyOption?: boolean;
variant?: 'default' | 'flat';
containerClassName?: string;
}

export const SelectInput = forwardRef<HTMLSelectElement, SelectInputProps>(function SelectInput(
Expand All @@ -22,43 +31,88 @@ export const SelectInput = forwardRef<HTMLSelectElement, SelectInputProps>(funct
helperText,
error,
placeholder,
options,
options = [],
groups = [],
size = 'default',
className = '',
containerClassName = '',
allowEmptyOption = false,
variant = 'default',
value,
disabled,
onChange,
defaultValue,
...props
},
ref,
) {
const sizeClasses = size === 'sm' ? 'h-10 px-3 text-sm' : 'px-4 py-3';
const variantClasses =
variant === 'flat'
? 'border-transparent bg-transparent px-0 py-0 h-auto shadow-none focus:ring-0 focus:border-transparent rounded-none'
: 'rounded-[10px] border border-[var(--agyn-border-subtle)] bg-white';

const sizeClasses =
variant === 'flat'
? size === 'sm'
? 'text-sm'
: 'text-base'
: size === 'sm'
? 'h-10 px-3 text-sm'
: 'px-4 py-3';

const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
onChange?.(event);
};

const selectProps =
value !== undefined
? { value: value ?? '' }
: defaultValue !== undefined
? { defaultValue }
: placeholder
? { defaultValue: '' }
: {};

const hasGroups = groups.length > 0;

return (
<div className="w-full">
<div className={cn('w-full', containerClassName)}>
{label ? <label className="mb-2 block text-[var(--agyn-dark)]">{label}</label> : null}
<select
ref={ref}
className={cn(
'w-full rounded-[10px] border border-[var(--agyn-border-subtle)] bg-white text-[var(--agyn-dark)]',
'focus:outline-none focus:ring-2 focus:ring-[var(--agyn-blue)] focus:border-transparent',
'w-full text-[var(--agyn-dark)] focus:outline-none focus:ring-2 focus:ring-[var(--agyn-blue)] focus:border-transparent',
'disabled:bg-[var(--agyn-bg-light)] disabled:cursor-not-allowed',
error ? 'border-red-500 focus:ring-red-500' : '',
variantClasses,
sizeClasses,
className,
)}
value={value ?? ''}
disabled={disabled}
onChange={handleChange}
{...selectProps}
{...props}
>
{placeholder ? (
<option value="" disabled={!allowEmptyOption} hidden={!allowEmptyOption}>
{placeholder}
</option>
) : null}
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
{hasGroups
? groups.map((group, groupIndex) => (
<optgroup key={group.label ?? groupIndex} label={group.label ?? ''}>
{group.options.map((option) => (
<option key={option.value} value={option.value} disabled={option.disabled}>
{option.label}
</option>
))}
</optgroup>
))
: options.map((option) => (
<option key={option.value} value={option.value} disabled={option.disabled}>
{option.label}
</option>
))}
</select>
{error ? <p className="mt-2 text-sm text-red-500">{error}</p> : null}
{!error && helperText ? (
Expand Down
35 changes: 32 additions & 3 deletions packages/platform-ui/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, type ReactNode } from 'react';
import { useEffect, useMemo, useState, type ReactNode } from 'react';
import {
ChevronRight,
User
Expand Down Expand Up @@ -31,10 +31,39 @@ interface SidebarProps {
export default function Sidebar({
menuItems,
currentUser = { name: 'John Doe', email: 'john@agyn.io' },
selectedMenuItem = 'graph',
selectedMenuItem = 'entitiesAgents',
onMenuItemSelect
}: SidebarProps) {
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set(['agents']));
const parentByChild = useMemo(() => {
const map = new Map<string, string>();
for (const item of menuItems) {
if (!item.items) continue;
for (const sub of item.items) {
map.set(sub.id, item.id);
}
}
return map;
}, [menuItems]);

const defaultExpandedSection = 'agents';

const [expandedItems, setExpandedItems] = useState<Set<string>>(() => {
const initial = new Set<string>();
const parent = parentByChild.get(selectedMenuItem ?? '');
initial.add(parent ?? defaultExpandedSection);
return initial;
});

useEffect(() => {
const parent = parentByChild.get(selectedMenuItem ?? '');
if (!parent) return;
setExpandedItems((prev) => {
if (prev.has(parent)) return prev;
const next = new Set(prev);
next.add(parent);
return next;
});
}, [selectedMenuItem, parentByChild]);

const toggleExpand = (id: string) => {
setExpandedItems(prev => {
Expand Down
Loading