Files
Teamup/client/src/components/AppShell.tsx
T
soroush.asadi 0bcf16e77f Agent profiles (AGENTS.md): per-org library, free builtins, versioning, marketplace, persona
Reusable agent definitions authored as AGENTS.md (YAML frontmatter + a Markdown body that becomes
the agent's operating guide). Mirrors the skill library, including its review hardening.

- AgentProfile entity (OrgBoard): org-scoped + versioned by (OrganizationId, ProfileKey, Version),
  NULLS NOT DISTINCT unique index; Origin Builtin|Authored|Installed; ProfileVisibility +
  ProfileStatus with the Public⟹Published invariant enforced in Apply()/SetVisibility(). AGENTS.md
  parser (YamlDotNet). AgentProfileWriter is the single upsert path (insert-only mode for install).
- Free builtins: AgentProfileSeeder seeds Aria (PO), Quill (QA), Edison (backend) on startup via a
  new IStartupSeeder + SeederRunner (runs after migrations). Idempotent, null-org, visible to all.
- Endpoints (/api/orgboard/agent-profiles): upload, list (resolvable-winner order), get versions,
  publish/unpublish, fork, marketplace (per-(key,version) AlreadyInLibrary), install (insert-only →
  clean 409, no clobber). ConfigureAgents to author/manage; ViewBoard to browse; audited.
- Persona: Agent gains Persona; ConfigureAgent stores it; AgentRunContext carries it; PromptAssembler
  injects it as "# Operating guide" (data, not instructions) so an applied profile shapes the run.
- Client: Agent profiles page (library + marketplace tabs, upload editor, publish/unlist/fork/install),
  routed + in the nav.

Verified: ArchitectureTests 8/8, IntegrationTests 55/55 (new AgentProfilesTests: builtins seeded,
upload + validation, publish, cross-org marketplace list→install→private copy, duplicate 409, per-
version flag, Member 403; persona renders as the operating guide), client build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 09:18:37 +03:30

115 lines
3.5 KiB
TypeScript

import type { ReactNode } from 'react'
import { Link, useLocation } from 'react-router'
import {
BookMarked,
BookUser,
Bot,
Boxes,
ChartColumn,
Gauge,
Inbox,
type LucideIcon,
LayoutDashboard,
LogOut,
Network,
ShieldCheck,
Users,
} from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { cn } from '@/lib/utils'
import { useAuth } from '@/store/auth'
export function AppShell({ children }: { children: ReactNode }) {
const email = useAuth((s) => s.email)
const logout = useAuth((s) => s.logout)
return (
<div className="flex min-h-screen bg-background text-foreground">
<aside className="flex w-60 shrink-0 flex-col bg-sidebar text-sidebar-foreground">
<div className="flex items-center gap-3 px-5 py-4">
<span className="grid size-8 place-items-center rounded-md bg-sidebar-primary font-bold text-sidebar-primary-foreground">
T
</span>
<div className="leading-tight">
<div className="font-semibold tracking-tight">TeamUp.AI</div>
<div className="text-xs text-sidebar-foreground/60">command center</div>
</div>
</div>
<Separator className="bg-sidebar-border" />
<nav className="flex flex-1 flex-col gap-1 p-3">
<NavItem icon={LayoutDashboard} label="Board" to="/" />
<NavItem icon={Inbox} label="Cartable" to="/cartable" />
<NavItem icon={ShieldCheck} label="Review inbox" to="/reviews" />
<NavItem icon={Bot} label="AI seats" to="/seats" />
<NavItem icon={BookUser} label="Agent profiles" to="/agent-profiles" />
<NavItem icon={BookMarked} label="Skills" to="/skills" />
<NavItem icon={Network} label="Org chart" to="/org" />
<NavItem icon={Boxes} label="Structure" to="/structure" />
<NavItem icon={Users} label="Members" to="/members" />
<NavItem icon={Gauge} label="Performance" to="/performance" />
<NavItem icon={ChartColumn} label="Analytics" to="/analytics" />
</nav>
<Separator className="bg-sidebar-border" />
<div className="flex items-center justify-between gap-2 p-3">
<span className="truncate text-xs text-sidebar-foreground/70">{email ?? 'signed in'}</span>
<Button
variant="ghost"
size="sm"
onClick={logout}
className="text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
>
<LogOut data-icon="inline-start" />
Sign out
</Button>
</div>
</aside>
<main className="flex-1 overflow-auto">{children}</main>
</div>
)
}
function NavItem({
icon: Icon,
label,
to,
muted,
}: {
icon: LucideIcon
label: string
to?: string
muted?: boolean
}) {
const location = useLocation()
const active = to ? location.pathname === to : false
const className = cn(
'flex items-center gap-3 rounded-md px-3 py-2 text-sm',
active ? 'bg-sidebar-accent font-medium text-sidebar-accent-foreground' : 'text-sidebar-foreground/80',
muted ? 'opacity-50' : 'hover:bg-sidebar-accent/60',
)
const content = (
<>
<Icon className="size-4" />
{label}
{muted && <span className="ml-auto text-[10px] uppercase tracking-wide opacity-70">soon</span>}
</>
)
if (!to || muted) {
return <span className={className}>{content}</span>
}
return (
<Link to={to} className={className}>
{content}
</Link>
)
}