Rebuild AgentCompany Electron app

This commit is contained in:
ZacharyZhang-NY committed 2026-05-20 23:27:00 -04:00
1 parent bc5daa6eb8
commit fcf6d90731
56 files changed
+8667

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
import type { DesktopApi } from "../shared/desktop-api";
declare global {
interface Window {
agentCompany: DesktopApi;
}
}
+16
View File
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:;"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AgentCompany</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
@@ -0,0 +1,47 @@
import { useState } from "react";
import { Shell, type RouteId } from "../shared/Shell";
import { Onboarding } from "./Onboarding";
import { useSnapshot } from "./useSnapshot";
import { HomePage } from "../pages/HomePage";
import { TeamPage } from "../pages/TeamPage";
import { WorkPage } from "../pages/WorkPage";
import { ChatPage } from "../pages/ChatPage";
import { FilesPage } from "../pages/FilesPage";
import { ApprovalsPage } from "../pages/ApprovalsPage";
import { SocialPage } from "../pages/SocialPage";
import { SettingsPage } from "../pages/SettingsPage";
export function App() {
const { snapshot, error, actions } = useSnapshot();
const [route, setRoute] = useState<RouteId>("home");
if (!snapshot) {
return <div className="app-shell" style={{ display: "grid", placeItems: "center" }} />;
}
if (snapshot.organizations.length === 0) {
return <Onboarding actions={actions} />;
}
const pageProps = { snapshot, actions };
const page = {
home: <HomePage {...pageProps} />,
team: <TeamPage {...pageProps} />,
work: <WorkPage {...pageProps} />,
chat: <ChatPage {...pageProps} />,
files: <FilesPage {...pageProps} />,
approvals: <ApprovalsPage {...pageProps} />,
social: <SocialPage {...pageProps} />,
settings: <SettingsPage {...pageProps} />,
}[route];
return (
<Shell snapshot={snapshot} route={route} setRoute={setRoute}>
{error && (
<div className="card" style={{ marginBottom: 16, color: "var(--red)" }}>
{error}
</div>
)}
{page}
</Shell>
);
}
@@ -0,0 +1,46 @@
import type { FormEvent } from "react";
import { Button, Field } from "../shared/ui";
import type { SnapshotActions } from "./useSnapshot";
function value(form: FormData, key: string): string {
return String(form.get(key) ?? "").trim();
}
export function Onboarding({ actions }: { actions: SnapshotActions }) {
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
void actions.run(() => window.agentCompany.createOrganization({
organizationName: value(form, "organizationName"),
mission: value(form, "mission"),
goal: value(form, "goal"),
employeeName: value(form, "employeeName"),
}));
};
return (
<main className="app-shell" style={{ display: "grid", placeItems: "center", padding: 24 }}>
<form className="card" style={{ width: "min(560px, 100%)", display: "grid", gap: 14 }} onSubmit={submit}>
<div>
<h1 className="page-h1">AgentCompany</h1>
<p className="page-sub" style={{ marginBottom: 0 }}>Create the company control plane.</p>
</div>
<Field label="Company name">
<input className="input" name="organizationName" required autoFocus />
</Field>
<Field label="Your name">
<input className="input" name="employeeName" required />
</Field>
<Field label="Mission">
<textarea className="input" name="mission" required />
</Field>
<Field label="First goal">
<textarea className="input" name="goal" required />
</Field>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<Button tone="primary" type="submit">Create</Button>
</div>
</form>
</main>
);
}
@@ -0,0 +1,49 @@
import { useCallback, useEffect, useState } from "react";
import type { AppSnapshot } from "../../../shared/domain";
import type { DesktopResult } from "../../../shared/result";
export interface SnapshotActions {
run<T extends AppSnapshot | null>(operation: () => Promise<DesktopResult<T>>): Promise<T | null>;
reload(): Promise<void>;
}
export function useSnapshot(): {
snapshot: AppSnapshot | null;
error: string | null;
actions: SnapshotActions;
} {
const [snapshot, setSnapshot] = useState<AppSnapshot | null>(null);
const [error, setError] = useState<string | null>(null);
const load = useCallback(async () => {
const result = await window.agentCompany.loadSnapshot();
if (result.ok) {
setSnapshot(result.data);
setError(null);
} else {
setError(result.error.message);
}
}, []);
const run = useCallback(async <T extends AppSnapshot | null>(
operation: () => Promise<DesktopResult<T>>,
): Promise<T | null> => {
const result = await operation();
if (result.ok) {
if (result.data) setSnapshot(result.data as AppSnapshot);
setError(null);
return result.data;
}
setError(result.error.message);
return null;
}, []);
useEffect(() => {
void Promise.resolve().then(load);
return window.agentCompany.subscribe((event) => {
if (event.type === "snapshot-changed") void load();
});
}, [load]);
return { snapshot, error, actions: { run, reload: load } };
}
@@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./app/App";
import "./styles.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
@@ -0,0 +1,95 @@
import { Avatar, Button, Icon, Pill } from "../shared/ui";
import type { ApprovalRecord } from "../../../shared/domain";
import type { PageProps } from "./types";
import { activeOrgId } from "./types";
export function ApprovalsPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const employee = snapshot.employees.find((item) => item.orgId === orgId);
const approvals = snapshot.approvals.filter((item) => item.orgId === orgId);
const pending = approvals.filter((item) => item.state === "pending");
const decided = approvals.filter((item) => item.state !== "pending");
const decide = (approvalId: string, state: "approved" | "declined") => {
if (!employee) return;
void actions.run(() => window.agentCompany.decideApproval({
orgId,
approvalId,
state,
decidedByEmployeeId: employee.id,
}));
};
return (
<div className="page">
<h1 className="page-h1">Approvals</h1>
<p className="page-sub">{pending.length} pending · {decided.length} decided</p>
<div className="grid-4" style={{ marginBottom: 24 }}>
<StatTile label="Pending" value={String(pending.length)} tone="amber" />
<StatTile label="Approved" value={String(decided.filter((item) => item.state === "approved").length)} />
<StatTile label="Declined" value={String(decided.filter((item) => item.state === "declined").length)} />
<StatTile label="High risk" value={String(pending.filter((item) => item.risk === "high").length)} />
</div>
<div className="section-label">Pending</div>
{pending.length === 0 ? (
<div className="card" style={{ color: "var(--text-3)" }}>No pending approvals</div>
) : pending.map((approval) => (
<ApprovalRow key={approval.id} approval={approval} onApprove={() => decide(approval.id, "approved")} onDecline={() => decide(approval.id, "declined")} />
))}
<div className="section-label" style={{ marginTop: 32 }}>Decided</div>
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
{decided.length === 0 ? (
<div style={{ padding: 18, color: "var(--text-3)" }}>No decisions</div>
) : decided.map((approval) => (
<div key={approval.id} className="table-row" style={{ gridTemplateColumns: "24px 1fr 120px" }}>
<Icon name="check" />
<span>{approval.title}</span>
<Pill tone={approval.state === "approved" ? "green" : "amber"}>{approval.state}</Pill>
</div>
))}
</div>
</div>
);
}
function StatTile({ label, value, tone }: { label: string; value: string; tone?: "amber" }) {
return (
<div className="card">
<div className="card-label" style={{ marginBottom: 6 }}>{label}</div>
<div className="num" style={{ fontSize: 22, fontWeight: 600, color: tone === "amber" ? "var(--amber)" : "var(--text-1)" }}>{value}</div>
</div>
);
}
function ApprovalRow({
approval,
onApprove,
onDecline,
}: {
approval: ApprovalRecord;
onApprove: () => void;
onDecline: () => void;
}) {
const riskColor = approval.risk === "high" ? "var(--red)" : approval.risk === "medium" ? "var(--amber)" : "var(--text-3)";
return (
<div className="card" style={{ marginBottom: 10, display: "grid", gridTemplateColumns: "6px 1fr auto", gap: 16 }}>
<div style={{ width: 3, alignSelf: "stretch", background: riskColor, borderRadius: 2 }} />
<div>
<div className="row" style={{ marginBottom: 4 }}>
<span style={{ color: riskColor, fontSize: 11, fontWeight: 540 }}>{approval.risk}</span>
<Pill>{approval.kind}</Pill>
</div>
<div style={{ fontSize: 15, fontWeight: 540, marginBottom: 4 }}>{approval.title}</div>
<div style={{ color: "var(--text-2)", lineHeight: 1.5, marginBottom: 12 }}>{approval.details}</div>
<div className="row">
<Avatar name={approval.requestedByType} size="sm" bot={approval.requestedByType !== "employee"} />
<span style={{ color: "var(--text-3)", fontSize: 12 }}>{new Date(approval.createdAt).toLocaleString()}</span>
</div>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 6, alignItems: "flex-end" }}>
<Button tone="primary" onClick={onApprove}>Approve</Button>
<Button tone="ghost" onClick={onDecline}>Decline</Button>
</div>
</div>
);
}
@@ -0,0 +1,158 @@
import { useState, type FormEvent, type ReactNode } from "react";
import { Avatar, Button, Icon, IconButton } from "../shared/ui";
import type { ChatSpaceRecord } from "../../../shared/domain";
import type { PageProps } from "./types";
import { activeOrgId } from "./types";
function messageBody(form: FormData): string {
return String(form.get("message") ?? "").trim();
}
export function ChatPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const spaces = snapshot.chatSpaces.filter((item) => item.orgId === orgId);
const employees = snapshot.employees.filter((item) => item.orgId === orgId);
const agents = snapshot.agents.filter((item) => item.orgId === orgId);
const [spaceId, setSpaceId] = useState(spaces[0]?.id ?? "");
const activeSpace = spaces.find((space) => space.id === spaceId) ?? spaces[0];
const messages = snapshot.chatMessages.filter((item) => item.orgId === orgId && item.spaceId === activeSpace?.id);
const sender = employees[0];
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!activeSpace || !sender) return;
const form = new FormData(event.currentTarget);
const bodyMd = messageBody(form);
if (!bodyMd) return;
event.currentTarget.reset();
void actions.run(() => window.agentCompany.sendMessage({
orgId,
spaceId: activeSpace.id,
threadId: null,
senderType: "employee",
senderId: sender.id,
bodyMd,
}));
};
return (
<div className="split-main" style={{ height: "calc(100vh - 112px)", margin: "-28px -32px -40px" }}>
<aside className="secondary-pane">
<div className="row" style={{ padding: "4px 8px 12px" }}>
<div style={{ fontWeight: 600 }}>Messages</div>
<IconButton icon="plus" label="New space" />
</div>
<NavGroup label="Channels">
{spaces.map((space) => (
<SpaceItem
key={space.id}
space={space}
active={activeSpace?.id === space.id}
count={snapshot.chatMessages.filter((message) => message.spaceId === space.id).length}
onClick={() => setSpaceId(space.id)}
/>
))}
</NavGroup>
<NavGroup label="Agents">
{agents.map((agent) => (
<div key={agent.id} className="nav-item">
<Avatar name={agent.name} size="sm" bot />
<span>{agent.name}</span>
</div>
))}
</NavGroup>
</aside>
<section style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", background: "var(--panel)" }}>
<header className="row" style={{ height: 52, padding: "0 24px", borderBottom: "1px solid var(--border-soft)" }}>
<Icon name={activeSpace?.visibility === "restricted" ? "lock" : "hash"} />
<div style={{ fontSize: 14, fontWeight: 600 }}>{activeSpace?.name ?? "Messages"}</div>
<div style={{ color: "var(--text-3)", borderLeft: "1px solid var(--border)", paddingLeft: 10 }}>
{activeSpace?.topic ?? ""}
</div>
<div style={{ marginLeft: "auto" }}><Icon name="search" /></div>
</header>
<div style={{ flex: 1, overflow: "auto", padding: "20px 24px 8px" }}>
{messages.length === 0 ? (
<div style={{ color: "var(--text-3)" }}>No messages</div>
) : messages.map((message) => (
<MessageRow
key={message.id}
body={message.bodyMd}
time={new Date(message.createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
name={displayName(message.senderType, message.senderId, employees, agents)}
bot={message.senderType === "agent"}
/>
))}
</div>
<form onSubmit={submit} style={{ padding: 14, paddingTop: 4 }}>
<div className="card" style={{ padding: "10px 12px" }}>
<textarea className="input" name="message" rows={2} placeholder={activeSpace ? `Message #${activeSpace.name}` : "Message"} />
<div className="row" style={{ paddingTop: 8, borderTop: "1px solid var(--border-soft)" }}>
<Icon name="attach" />
<Icon name="social" />
<div style={{ marginLeft: "auto" }}>
<Button tone="primary" size="small" type="submit"><Icon name="send" /> Send</Button>
</div>
</div>
</div>
</form>
</section>
</div>
);
}
function NavGroup({ label, children }: { label: string; children: ReactNode }) {
return (
<div style={{ marginTop: 10 }}>
<div className="section-label" style={{ padding: "0 8px", marginBottom: 6 }}>{label}</div>
{children}
</div>
);
}
function SpaceItem({
space,
active,
count,
onClick,
}: {
space: ChatSpaceRecord;
active: boolean;
count: number;
onClick: () => void;
}) {
return (
<button type="button" className={`nav-item ${active ? "active" : ""}`} onClick={onClick}>
<Icon name={space.visibility === "restricted" ? "lock" : "hash"} />
<span>{space.name}</span>
{count > 0 && <span className="nav-badge">{count}</span>}
</button>
);
}
function MessageRow({ name, body, time, bot }: { name: string; body: string; time: string; bot: boolean }) {
return (
<div style={{ display: "flex", gap: 12, marginBottom: 18 }}>
<Avatar name={name} bot={bot} />
<div style={{ minWidth: 0, flex: 1 }}>
<div className="row" style={{ alignItems: "baseline", marginBottom: 2 }}>
<span style={{ fontWeight: 540 }}>{name}</span>
<span className="num" style={{ color: "var(--text-3)", fontSize: 11 }}>{time}</span>
</div>
<div style={{ lineHeight: 1.5, whiteSpace: "pre-wrap" }}>{body}</div>
</div>
</div>
);
}
function displayName(
senderType: string,
senderId: string | null,
employees: Array<{ id: string; displayName: string }>,
agents: Array<{ id: string; name: string }>,
): string {
if (senderType === "employee") return employees.find((item) => item.id === senderId)?.displayName ?? "Employee";
if (senderType === "agent") return agents.find((item) => item.id === senderId)?.name ?? "Agent";
if (senderType === "board") return "Board";
return "System";
}
@@ -0,0 +1,112 @@
import { Button, Icon, IconButton } from "../shared/ui";
import type { FileObjectRecord } from "../../../shared/domain";
import type { PageProps } from "./types";
import { activeOrgId } from "./types";
export function FilesPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const files = snapshot.files.filter((item) => item.orgId === orgId);
return (
<div style={{ display: "grid", gridTemplateColumns: "200px minmax(0, 1fr)", gap: 24 }}>
<aside>
<div className="section-label">Library</div>
<TreeItem icon="folder" label="All files" count={files.length} active />
<TreeItem icon="social" label="Recent" />
<TreeItem icon="check" label="Pinned" />
</aside>
<div className="page">
<div className="row" style={{ marginBottom: 24 }}>
<div>
<h1 className="page-h1">Files</h1>
<p className="page-sub" style={{ marginBottom: 0 }}>{files.length} files · {formatBytes(files.reduce((sum, file) => sum + file.sizeBytes, 0))}</p>
</div>
<div style={{ marginLeft: "auto" }}>
<Button tone="primary" onClick={() => void actions.run(() => window.agentCompany.importFile({ orgId }))}>
<Icon name="upload" /> Upload
</Button>
</div>
</div>
<div className="section-label">All files</div>
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
<div className="table-row" style={{ gridTemplateColumns: "3fr 1fr 1.4fr 1fr 40px", background: "var(--panel-2)", color: "var(--text-3)", fontSize: 11 }}>
<div>Name</div><div>Size</div><div>Checksum</div><div>Added</div><div />
</div>
{files.length === 0 ? (
<div style={{ padding: 18, color: "var(--text-3)" }}>No files</div>
) : files.map((file) => <FileRow key={file.id} file={file} />)}
</div>
</div>
</div>
);
}
function TreeItem({
icon,
label,
count,
active = false,
}: {
icon: string;
label: string;
count?: number;
active?: boolean;
}) {
return (
<div className={`nav-item ${active ? "active" : ""}`}>
<Icon name={icon} />
<span>{label}</span>
{typeof count === "number" && <span className="nav-badge">{count}</span>}
</div>
);
}
function FileRow({ file }: { file: FileObjectRecord }) {
return (
<div className="table-row" style={{ gridTemplateColumns: "3fr 1fr 1.4fr 1fr 40px" }}>
<div className="row">
<FileIcon kind={file.kind} />
<span style={{ fontWeight: 480 }}>{file.name}</span>
</div>
<div className="num" style={{ color: "var(--text-3)", fontSize: 12 }}>{formatBytes(file.sizeBytes)}</div>
<div className="num" style={{ color: "var(--text-3)", fontSize: 12 }}>{file.sha256.slice(0, 12)}</div>
<div style={{ color: "var(--text-3)", fontSize: 12 }}>{new Date(file.createdAt).toLocaleDateString()}</div>
<IconButton icon="more" label="More" />
</div>
);
}
function FileIcon({ kind }: { kind: string }) {
const colors: Record<string, string> = {
md: "#52525b",
pdf: "#d23b3b",
img: "#c87d12",
sheet: "#2f9e58",
doc: "#5e6ad2",
};
if (kind === "folder") return <Icon name="folder" />;
return (
<span
style={{
width: 22,
height: 22,
display: "grid",
placeItems: "center",
borderRadius: 4,
background: `${colors[kind] ?? "#a1a1aa"}14`,
color: colors[kind] ?? "#52525b",
fontSize: 8,
fontWeight: 700,
}}
>
{kind.slice(0, 3).toUpperCase()}
</span>
);
}
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
}
@@ -0,0 +1,75 @@
import { Icon, Pill } from "../shared/ui";
import type { PageProps } from "./types";
import { activeOrgId, money } from "./types";
export function HomePage({ snapshot }: PageProps) {
const orgId = activeOrgId(snapshot);
const organization = snapshot.organizations.find((item) => item.id === orgId)!;
const tasks = snapshot.tasks.filter((item) => item.orgId === orgId);
const approvals = snapshot.approvals.filter((item) => item.orgId === orgId);
const agents = snapshot.agents.filter((item) => item.orgId === orgId);
const providers = snapshot.providers.filter((item) => item.orgId === orgId && item.enabled);
const budget = agents.reduce((sum, agent) => sum + agent.budgetCents, 0);
const pending = approvals.filter((item) => item.state === "pending");
return (
<div className="page">
<h1 className="page-h1">{organization.name}</h1>
<p className="page-sub">{organization.goal}</p>
<div className="grid-3" style={{ marginBottom: 12 }}>
<MetricCard label="Agents" value={String(agents.length)} />
<MetricCard label="Open work" value={String(tasks.filter((item) => item.status !== "done").length)} />
<MetricCard label="Monthly budget" value={money(budget)} />
</div>
<div className="grid-3">
<div className="card" style={{ gridColumn: "span 2" }}>
<div className="card-label" style={{ marginBottom: 14 }}>Work</div>
{tasks.length === 0 ? <EmptyLine label="No tasks" /> : tasks.slice(0, 6).map((task) => (
<div key={task.id} className="row" style={{ padding: "10px 0", borderBottom: "1px solid var(--border-soft)" }}>
<span style={{ width: 6, height: 6, borderRadius: "50%", background: "var(--text-4)" }} />
<span style={{ flex: 1, fontWeight: 480 }}>{task.title}</span>
<Pill>{task.status.replaceAll("_", " ")}</Pill>
</div>
))}
</div>
<div className="card">
<div className="card-label" style={{ marginBottom: 14 }}>Decisions</div>
{pending.length === 0 ? <EmptyLine label="No pending approvals" /> : pending.slice(0, 4).map((approval) => (
<div key={approval.id} style={{ padding: "10px 0", borderBottom: "1px solid var(--border-soft)" }}>
<div style={{ fontWeight: 540, marginBottom: 4 }}>{approval.title}</div>
<Pill tone={approval.risk === "high" ? "amber" : undefined}>{approval.risk}</Pill>
</div>
))}
</div>
</div>
<div className="grid-3" style={{ marginTop: 12 }}>
<div className="card">
<div className="card-label" style={{ marginBottom: 14 }}>Providers</div>
{providers.length === 0 ? <EmptyLine label="No connected providers" /> : providers.map((provider) => (
<div key={provider.id} className="row" style={{ padding: "8px 0" }}>
<Icon name="check" />
<span>{provider.label}</span>
</div>
))}
</div>
<div className="card" style={{ gridColumn: "span 2" }}>
<div className="card-label" style={{ marginBottom: 14 }}>Mission</div>
<div style={{ fontSize: 15, lineHeight: 1.5 }}>{organization.mission}</div>
</div>
</div>
</div>
);
}
function MetricCard({ label, value }: { label: string; value: string }) {
return (
<div className="card">
<div className="card-label" style={{ marginBottom: 8 }}>{label}</div>
<div className="num" style={{ fontSize: 28, fontWeight: 600 }}>{value}</div>
</div>
);
}
function EmptyLine({ label }: { label: string }) {
return <div style={{ color: "var(--text-3)", padding: "10px 0" }}>{label}</div>;
}
@@ -0,0 +1,215 @@
import { useState, type FormEvent } from "react";
import { Button, Field, Icon, Pill } from "../shared/ui";
import { Dialog } from "../shared/Dialog";
import type { ProviderRecord, SkillDefinitionRecord } from "../../../shared/domain";
import type { PageProps } from "./types";
import { activeOrgId } from "./types";
type SettingsTab = "providers" | "skills" | "policies" | "migration";
function value(form: FormData, key: string): string {
return String(form.get(key) ?? "").trim();
}
export function SettingsPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const [tab, setTab] = useState<SettingsTab>("providers");
const [providerDialog, setProviderDialog] = useState<ProviderRecord | null>(null);
const [skillOpen, setSkillOpen] = useState(false);
const providers = snapshot.providers.filter((item) => item.orgId === orgId);
const skills = snapshot.skillDefinitions.filter((item) => item.orgId === orgId);
const agents = snapshot.agents.filter((item) => item.orgId === orgId);
const revisions = snapshot.skillRevisions;
const saveProvider = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!providerDialog) return;
const form = new FormData(event.currentTarget);
void actions.run(() => window.agentCompany.saveProviderCredential({
orgId,
providerId: providerDialog.id,
secret: value(form, "secret"),
model: value(form, "model") || null,
})).then(() => setProviderDialog(null));
};
const importSkill = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
void actions.run(() => window.agentCompany.importSkillDirectory({
orgId,
directoryPath: value(form, "directoryPath"),
versionSemver: value(form, "versionSemver") || "1.0.0",
})).then(() => setSkillOpen(false));
};
return (
<div className="split-main" style={{ height: "calc(100vh - 112px)", margin: "-28px -32px -40px" }}>
<aside className="secondary-pane">
<div style={{ fontWeight: 600, padding: "4px 8px 14px" }}>Settings</div>
<SettingsNav label="Providers" active={tab === "providers"} onClick={() => setTab("providers")} />
<SettingsNav label="Skills" active={tab === "skills"} onClick={() => setTab("skills")} />
<SettingsNav label="Policies" active={tab === "policies"} onClick={() => setTab("policies")} />
<SettingsNav label="Migration" active={tab === "migration"} onClick={() => setTab("migration")} />
</aside>
<div className="content" style={{ flex: 1 }}>
{tab === "providers" && (
<ProvidersPanel providers={providers} onManage={setProviderDialog} />
)}
{tab === "skills" && (
<SkillsPanel
skills={skills}
agents={agents}
revisions={revisions}
onImport={() => setSkillOpen(true)}
onBind={(agentId, skill, revisionId) => void actions.run(() => window.agentCompany.bindSkill({
orgId,
agentId,
skillId: skill.id,
revisionId,
}))}
/>
)}
{tab === "policies" && <PolicyPanel />}
{tab === "migration" && <MigrationPanel />}
</div>
<Dialog title="Provider credential" open={Boolean(providerDialog)} submitLabel="Save" onClose={() => setProviderDialog(null)} onSubmit={saveProvider}>
<Field label="Provider"><input className="input" value={providerDialog?.label ?? ""} readOnly /></Field>
<Field label="Secret"><input className="input" name="secret" type="password" required /></Field>
<Field label="Model"><input className="input" name="model" defaultValue={providerDialog?.model ?? ""} /></Field>
</Dialog>
<Dialog title="Import skill" open={skillOpen} submitLabel="Import" onClose={() => setSkillOpen(false)} onSubmit={importSkill}>
<Field label="Directory path"><input className="input" name="directoryPath" required /></Field>
<Field label="Version"><input className="input" name="versionSemver" defaultValue="1.0.0" required /></Field>
</Dialog>
</div>
);
}
function SettingsNav({ label, active, onClick }: { label: string; active: boolean; onClick: () => void }) {
return <button type="button" className={`nav-item ${active ? "active" : ""}`} onClick={onClick}>{label}</button>;
}
function ProvidersPanel({ providers, onManage }: { providers: ProviderRecord[]; onManage: (provider: ProviderRecord) => void }) {
return (
<div className="page">
<h1 className="page-h1">Providers</h1>
<p className="page-sub">Keys, account grants, OAuth profiles.</p>
<div className="section-label">Configured providers</div>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{providers.map((provider) => (
<div key={provider.id} className="card row">
<ProviderLogo label={provider.label} />
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 540 }}>{provider.label}</div>
<div style={{ fontSize: 12, color: "var(--text-3)" }}>{provider.authType.replaceAll("_", " ")}</div>
</div>
<Pill tone={provider.enabled ? "green" : undefined}>{provider.enabled ? "Enabled" : "Unconfigured"}</Pill>
<Button onClick={() => onManage(provider)}>Manage</Button>
</div>
))}
</div>
</div>
);
}
function SkillsPanel({
skills,
agents,
revisions,
onImport,
onBind,
}: {
skills: SkillDefinitionRecord[];
agents: Array<{ id: string; name: string }>;
revisions: Array<{ id: string; skillId: string; versionSemver: string }>;
onImport: () => void;
onBind: (agentId: string, skill: SkillDefinitionRecord, revisionId: string) => void;
}) {
return (
<div className="page">
<div className="row" style={{ marginBottom: 24 }}>
<div>
<h1 className="page-h1">Skills</h1>
<p className="page-sub" style={{ marginBottom: 0 }}>{skills.length} skills · {agents.length} agents</p>
</div>
<div style={{ marginLeft: "auto" }}>
<Button tone="primary" onClick={onImport}><Icon name="plus" /> Import</Button>
</div>
</div>
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
{skills.length === 0 ? (
<div style={{ padding: 18, color: "var(--text-3)" }}>No skills</div>
) : skills.map((skill) => {
const revision = revisions.find((item) => item.skillId === skill.id);
return (
<div key={skill.id} className="table-row" style={{ gridTemplateColumns: "1.6fr 1fr 1.6fr" }}>
<div>
<div style={{ fontWeight: 540 }}>{skill.name}</div>
<div style={{ color: "var(--text-3)", fontSize: 11 }}>{skill.slug}</div>
</div>
<Pill>{revision?.versionSemver ?? "No revision"}</Pill>
<select
className="input"
defaultValue=""
onChange={(event) => {
if (event.target.value && revision) onBind(event.target.value, skill, revision.id);
event.target.value = "";
}}
>
<option value="">Bind to agent</option>
{agents.map((agent) => <option key={agent.id} value={agent.id}>{agent.name}</option>)}
</select>
</div>
);
})}
</div>
</div>
);
}
function PolicyPanel() {
return (
<div className="page">
<h1 className="page-h1">Policies</h1>
<p className="page-sub">Approval, risk, provider, browser boundaries.</p>
<div className="card" style={{ display: "grid", gap: 10 }}>
<PolicyRow label="High-risk task" value="Board approval" />
<PolicyRow label="Social publish" value="Human approval" />
<PolicyRow label="Browser permissions" value="Deny by default" />
<PolicyRow label="Provider secrets" value="OS encrypted" />
</div>
</div>
);
}
function MigrationPanel() {
return (
<div className="page">
<h1 className="page-h1">Migration</h1>
<p className="page-sub">Legacy import, id maps, checksum validation.</p>
<div className="card" style={{ display: "grid", gap: 10 }}>
<PolicyRow label="Legacy snapshot" value="Immutable" />
<PolicyRow label="Entity checksums" value="Required" />
<PolicyRow label="Delta journal" value="Recorded" />
</div>
</div>
);
}
function PolicyRow({ label, value }: { label: string; value: string }) {
return (
<div className="row" style={{ justifyContent: "space-between" }}>
<span>{label}</span>
<Pill>{value}</Pill>
</div>
);
}
function ProviderLogo({ label }: { label: string }) {
return (
<div style={{ width: 30, height: 30, borderRadius: 7, display: "grid", placeItems: "center", background: "var(--text-1)", color: "#fff", fontWeight: 700 }}>
{label.slice(0, 2)}
</div>
);
}
@@ -0,0 +1,138 @@
import { useState, type FormEvent } from "react";
import { Avatar, Button, Field, Icon, Pill } from "../shared/ui";
import { Dialog } from "../shared/Dialog";
import { SOCIAL_PLATFORM, type SocialAccountRecord, type SocialPlatform } from "../../../shared/domain";
import type { PageProps } from "./types";
import { activeOrgId } from "./types";
function value(form: FormData, key: string): string {
return String(form.get(key) ?? "").trim();
}
export function SocialPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const [open, setOpen] = useState(false);
const accounts = snapshot.socialAccounts.filter((item) => item.orgId === orgId);
const employees = snapshot.employees.filter((item) => item.orgId === orgId);
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
void actions.run(() => window.agentCompany.createSocialAccount({
orgId,
platform: value(form, "platform") as SocialPlatform,
handle: value(form, "handle"),
displayName: value(form, "displayName"),
authorizedEmployeeId: value(form, "authorizedEmployeeId") || null,
})).then(() => setOpen(false));
};
return (
<div className="page">
<div className="row" style={{ marginBottom: 24 }}>
<div>
<h1 className="page-h1">Social</h1>
<p className="page-sub" style={{ marginBottom: 0 }}>{accounts.length} accounts</p>
</div>
<div style={{ marginLeft: "auto" }}>
<Button tone="primary" onClick={() => setOpen(true)}><Icon name="plus" /> Connect account</Button>
</div>
</div>
<div className="section-label">Accounts</div>
<div className="grid-4" style={{ marginBottom: 28 }}>
{accounts.length === 0 ? (
<div className="card" style={{ color: "var(--text-3)" }}>No social accounts</div>
) : accounts.map((account) => (
<AccountCard
key={account.id}
account={account}
employeeName={employees.find((employee) => employee.id === account.authorizedEmployeeId)?.displayName ?? null}
onLogin={() => void actions.run(() => window.agentCompany.launchSocialLogin({ socialAccountId: account.id }))}
/>
))}
</div>
<div className="section-label">Governance</div>
<div className="card" style={{ display: "grid", gap: 10 }}>
<PolicyRow label="Session isolation" value="Per account partition" />
<PolicyRow label="Permissions" value="Camera, microphone, downloads denied" />
<PolicyRow label="Navigation" value="Platform host allowlist" />
</div>
<Dialog title="Connect social account" open={open} submitLabel="Connect" onClose={() => setOpen(false)} onSubmit={submit}>
<Field label="Platform">
<select className="input" name="platform" defaultValue={SOCIAL_PLATFORM.X}>
<option value={SOCIAL_PLATFORM.X}>X</option>
<option value={SOCIAL_PLATFORM.LINKEDIN}>LinkedIn</option>
<option value={SOCIAL_PLATFORM.YOUTUBE}>YouTube</option>
<option value={SOCIAL_PLATFORM.INSTAGRAM}>Instagram</option>
</select>
</Field>
<Field label="Handle"><input className="input" name="handle" required /></Field>
<Field label="Display name"><input className="input" name="displayName" required /></Field>
<Field label="Authorized employee">
<select className="input" name="authorizedEmployeeId">
<option value="">Unassigned</option>
{employees.map((employee) => <option key={employee.id} value={employee.id}>{employee.displayName}</option>)}
</select>
</Field>
</Dialog>
</div>
);
}
function AccountCard({
account,
employeeName,
onLogin,
}: {
account: SocialAccountRecord;
employeeName: string | null;
onLogin: () => void;
}) {
return (
<div className="card" style={{ display: "flex", flexDirection: "column", gap: 10 }}>
<div className="row">
<PlatformIcon platform={account.platform} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 540 }}>{account.handle}</div>
<div style={{ color: "var(--text-3)", fontSize: 11 }}>{account.displayName}</div>
</div>
</div>
{employeeName && (
<div className="row" style={{ color: "var(--text-3)", fontSize: 11 }}>
<Avatar name={employeeName} size="sm" />
<span>{employeeName}</span>
</div>
)}
<Button size="small" onClick={onLogin}>Sign in</Button>
</div>
);
}
function PolicyRow({ label, value }: { label: string; value: string }) {
return (
<div className="row" style={{ justifyContent: "space-between" }}>
<span>{label}</span>
<Pill>{value}</Pill>
</div>
);
}
function PlatformIcon({ platform }: { platform: SocialPlatform }) {
const labels: Record<SocialPlatform, string> = {
x: "X",
linkedin: "in",
youtube: "▶",
instagram: "◎",
};
const colors: Record<SocialPlatform, string> = {
x: "#0a0a0b",
linkedin: "#0a66c2",
youtube: "#ff0000",
instagram: "#e1306c",
};
return (
<div style={{ width: 30, height: 30, borderRadius: 7, background: colors[platform], color: "#fff", display: "grid", placeItems: "center", fontWeight: 700 }}>
{labels[platform]}
</div>
);
}
@@ -0,0 +1,137 @@
import { useState, type FormEvent } from "react";
import { Avatar, Button, Field, Icon, Pill } from "../shared/ui";
import { Dialog } from "../shared/Dialog";
import type { PageProps } from "./types";
import { activeOrgId, money } from "./types";
function text(form: FormData, key: string): string {
return String(form.get(key) ?? "").trim();
}
export function TeamPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const [open, setOpen] = useState(false);
const employees = snapshot.employees.filter((item) => item.orgId === orgId);
const agents = snapshot.agents.filter((item) => item.orgId === orgId);
const providers = snapshot.providers.filter((item) => item.orgId === orgId && item.enabled);
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
void actions.run(() => window.agentCompany.createAgent({
orgId,
name: text(form, "name"),
role: text(form, "role"),
department: text(form, "department"),
modelProviderId: text(form, "providerId") || null,
modelId: text(form, "modelId") || null,
budgetCents: Math.max(0, Math.round(Number(text(form, "budget")) * 100)),
managerAgentId: text(form, "managerAgentId") || null,
})).then(() => setOpen(false));
};
return (
<div className="page">
<div className="row" style={{ marginBottom: 24 }}>
<div>
<h1 className="page-h1">Team</h1>
<p className="page-sub" style={{ marginBottom: 0 }}>{employees.length} human · {agents.length} agents</p>
</div>
<div style={{ marginLeft: "auto" }}>
<Button tone="primary" onClick={() => setOpen(true)}><Icon name="plus" /> Hire</Button>
</div>
</div>
<div className="card" style={{ padding: 32, marginBottom: 28 }}>
<div className="section-label">Org</div>
<div style={{ display: "flex", justifyContent: "center", marginBottom: 20 }}>
{employees[0] && <OrgCard name={employees[0].displayName} role={employees[0].role} human />}
</div>
<div className="grid-4">
{agents.map((agent) => (
<OrgCard key={agent.id} name={agent.name} role={agent.role} />
))}
</div>
</div>
<div className="section-label">Roster</div>
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
<div className="table-row" style={{ gridTemplateColumns: "2fr 1.2fr 1.4fr 1fr", background: "var(--panel-2)", fontSize: 11, color: "var(--text-3)" }}>
<div>Member</div><div>Department</div><div>Model</div><div>Budget</div>
</div>
{employees.map((employee) => (
<RosterRow key={employee.id} name={employee.displayName} role={employee.role} dept="Board" model="" budget={null} human />
))}
{agents.map((agent) => (
<RosterRow
key={agent.id}
name={agent.name}
role={agent.role}
dept={agent.department}
model={agent.modelId ?? ""}
budget={agent.budgetCents}
/>
))}
</div>
<Dialog title="Hire agent" open={open} submitLabel="Hire" onClose={() => setOpen(false)} onSubmit={submit}>
<Field label="Name"><input className="input" name="name" required /></Field>
<Field label="Role"><input className="input" name="role" required /></Field>
<Field label="Department"><input className="input" name="department" required /></Field>
<Field label="Provider">
<select className="input" name="providerId">
<option value="">Unassigned</option>
{providers.map((provider) => <option key={provider.id} value={provider.id}>{provider.label}</option>)}
</select>
</Field>
<Field label="Model"><input className="input" name="modelId" /></Field>
<Field label="Monthly budget"><input className="input" name="budget" type="number" min="0" step="1" defaultValue="0" /></Field>
</Dialog>
</div>
);
}
function OrgCard({ name, role, human = false }: { name: string; role: string; human?: boolean }) {
return (
<div className="card" style={{ display: "flex", flexDirection: "column", gap: 10 }}>
<div className="row">
<Avatar name={name} bot={!human} />
<div style={{ minWidth: 0 }}>
<div style={{ fontWeight: 540 }}>{name}</div>
<div style={{ fontSize: 11, color: "var(--text-3)" }}>{role}</div>
</div>
</div>
<Pill tone={human ? undefined : "violet"} dot={!human}>{human ? "Human" : "Agent"}</Pill>
</div>
);
}
function RosterRow({
name,
role,
dept,
model,
budget,
human = false,
}: {
name: string;
role: string;
dept: string;
model: string;
budget: number | null;
human?: boolean;
}) {
return (
<div className="table-row" style={{ gridTemplateColumns: "2fr 1.2fr 1.4fr 1fr" }}>
<div className="row">
<Avatar name={name} size="sm" bot={!human} />
<div>
<div style={{ fontWeight: 500 }}>{name}</div>
<div style={{ fontSize: 11, color: "var(--text-3)" }}>{role}</div>
</div>
</div>
<div style={{ color: "var(--text-2)" }}>{dept}</div>
<div style={{ color: "var(--text-2)", fontFamily: model ? "var(--font-mono)" : "inherit", fontSize: model ? 12 : 13 }}>
{model || "—"}
</div>
<div>{budget === null ? <span style={{ color: "var(--text-4)" }}>—</span> : <span className="num">{money(budget)}</span>}</div>
</div>
);
}
@@ -0,0 +1,130 @@
import { useState, type FormEvent } from "react";
import { Avatar, Button, Field, Icon, IconButton, Pill } from "../shared/ui";
import { Dialog } from "../shared/Dialog";
import { RISK_LEVEL, TASK_STATUS, type TaskRecord, type TaskStatus } from "../../../shared/domain";
import type { PageProps } from "./types";
import { activeOrgId } from "./types";
const columns: Array<{ id: TaskStatus; label: string }> = [
{ id: TASK_STATUS.BACKLOG, label: "Backlog" },
{ id: TASK_STATUS.IN_PROGRESS, label: "In progress" },
{ id: TASK_STATUS.REVIEW, label: "In review" },
{ id: TASK_STATUS.DONE, label: "Done" },
];
function value(form: FormData, key: string): string {
return String(form.get(key) ?? "").trim();
}
export function WorkPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const [open, setOpen] = useState(false);
const tasks = snapshot.tasks.filter((item) => item.orgId === orgId);
const agents = snapshot.agents.filter((item) => item.orgId === orgId);
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
void actions.run(() => window.agentCompany.createTask({
orgId,
title: value(form, "title"),
description: value(form, "description"),
status: TASK_STATUS.BACKLOG,
assigneeAgentId: value(form, "assigneeAgentId") || null,
risk: value(form, "risk") as "low" | "medium" | "high",
})).then(() => setOpen(false));
};
return (
<div className="page-wide">
<div className="row" style={{ marginBottom: 24 }}>
<div>
<h1 className="page-h1">Work</h1>
<p className="page-sub" style={{ marginBottom: 0 }}>{tasks.length} tasks · {agents.length} assignees</p>
</div>
<div style={{ marginLeft: "auto" }}>
<Button tone="primary" onClick={() => setOpen(true)}><Icon name="plus" /> New task</Button>
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(4, minmax(240px, 1fr))", gap: 12 }}>
{columns.map((column) => {
const columnTasks = tasks.filter((task) => task.status === column.id);
return (
<div key={column.id} style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div className="row" style={{ padding: "4px 6px 8px" }}>
<span style={{ width: 8, height: 8, borderRadius: 2, background: columnColor(column.id) }} />
<span style={{ fontSize: 12.5, fontWeight: 540 }}>{column.label}</span>
<span className="num" style={{ color: "var(--text-3)" }}>{columnTasks.length}</span>
</div>
{columnTasks.map((task) => (
<TaskCard
key={task.id}
task={task}
assignee={agents.find((agent) => agent.id === task.assigneeAgentId)?.name ?? null}
onMove={(status) => void actions.run(() => window.agentCompany.updateTaskStatus({ orgId, taskId: task.id, status }))}
/>
))}
</div>
);
})}
</div>
<Dialog title="New task" open={open} submitLabel="Create" onClose={() => setOpen(false)} onSubmit={submit}>
<Field label="Title"><input className="input" name="title" required /></Field>
<Field label="Description"><textarea className="input" name="description" required /></Field>
<Field label="Assignee">
<select className="input" name="assigneeAgentId">
<option value="">Unassigned</option>
{agents.map((agent) => <option key={agent.id} value={agent.id}>{agent.name}</option>)}
</select>
</Field>
<Field label="Risk">
<select className="input" name="risk" defaultValue={RISK_LEVEL.LOW}>
<option value={RISK_LEVEL.LOW}>Low</option>
<option value={RISK_LEVEL.MEDIUM}>Medium</option>
<option value={RISK_LEVEL.HIGH}>High</option>
</select>
</Field>
</Dialog>
</div>
);
}
function TaskCard({
task,
assignee,
onMove,
}: {
task: TaskRecord;
assignee: string | null;
onMove: (status: TaskStatus) => void;
}) {
return (
<div className="card" style={{ display: "flex", flexDirection: "column", gap: 10, opacity: task.status === "done" ? 0.7 : 1 }}>
<div style={{ display: "flex", alignItems: "flex-start", gap: 6 }}>
{task.risk === "high" && <span style={{ width: 3, height: 14, background: "var(--red)", borderRadius: 2, marginTop: 2 }} />}
<div style={{ fontWeight: 480, lineHeight: 1.35 }}>{task.title}</div>
</div>
<div className="row">
<Pill tone={task.risk === "high" ? "amber" : undefined}>{task.risk}</Pill>
{assignee && <Avatar name={assignee} size="sm" bot />}
<div style={{ marginLeft: "auto" }}>
{nextStatus(task.status) && <IconButton icon="check" label="Move" onClick={() => onMove(nextStatus(task.status)!)} />}
</div>
</div>
</div>
);
}
function columnColor(status: TaskStatus): string {
if (status === TASK_STATUS.IN_PROGRESS) return "var(--accent)";
if (status === TASK_STATUS.REVIEW) return "var(--amber)";
if (status === TASK_STATUS.DONE) return "var(--green)";
return "var(--text-4)";
}
function nextStatus(status: TaskStatus): TaskStatus | null {
if (status === TASK_STATUS.BACKLOG) return TASK_STATUS.IN_PROGRESS;
if (status === TASK_STATUS.IN_PROGRESS) return TASK_STATUS.REVIEW;
if (status === TASK_STATUS.REVIEW) return TASK_STATUS.DONE;
return null;
}
@@ -0,0 +1,18 @@
import type { AppSnapshot } from "../../../shared/domain";
import type { SnapshotActions } from "../app/useSnapshot";
export interface PageProps {
snapshot: AppSnapshot;
actions: SnapshotActions;
}
export function activeOrgId(snapshot: AppSnapshot): string {
const id = snapshot.activeOrgId;
if (!id) throw new Error("Active organization is required.");
return id;
}
export function money(cents: number): string {
return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 })
.format(cents / 100);
}
@@ -0,0 +1,32 @@
import type { FormEvent, ReactNode } from "react";
import { Button } from "./ui";
export function Dialog({
title,
open,
children,
submitLabel,
onClose,
onSubmit,
}: {
title: string;
open: boolean;
children: ReactNode;
submitLabel: string;
onClose: () => void;
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
}) {
if (!open) return null;
return (
<div className="modal-backdrop" role="presentation" onMouseDown={onClose}>
<form className="modal" onMouseDown={(event) => event.stopPropagation()} onSubmit={onSubmit}>
<h2 className="modal-title">{title}</h2>
<div style={{ display: "grid", gap: 12 }}>{children}</div>
<div className="modal-actions">
<Button tone="ghost" onClick={onClose}>Cancel</Button>
<Button tone="primary" type="submit">{submitLabel}</Button>
</div>
</form>
</div>
);
}
@@ -0,0 +1,86 @@
import type { ReactNode } from "react";
import type { AppSnapshot } from "../../../shared/domain";
import { Avatar, Icon, IconButton } from "./ui";
export type RouteId = "home" | "team" | "work" | "chat" | "files" | "approvals" | "social" | "settings";
const navItems: Array<{ id: RouteId; label: string; icon: string }> = [
{ id: "home", label: "Home", icon: "home" },
{ id: "team", label: "Team", icon: "team" },
{ id: "work", label: "Work", icon: "work" },
{ id: "chat", label: "Messages", icon: "chat" },
{ id: "files", label: "Files", icon: "docs" },
{ id: "approvals", label: "Approvals", icon: "approve" },
{ id: "social", label: "Social", icon: "social" },
{ id: "settings", label: "Settings", icon: "settings" },
];
export function Shell({
snapshot,
route,
setRoute,
actions,
children,
}: {
snapshot: AppSnapshot;
route: RouteId;
setRoute: (route: RouteId) => void;
actions?: ReactNode;
children: ReactNode;
}) {
const organization = snapshot.organizations.find((item) => item.id === snapshot.activeOrgId);
const employee = snapshot.employees.find((item) => item.orgId === snapshot.activeOrgId);
const badges = {
work: snapshot.tasks.filter((item) => item.orgId === snapshot.activeOrgId && item.status !== "done").length,
chat: snapshot.chatMessages.filter((item) => item.orgId === snapshot.activeOrgId).length,
approvals: snapshot.approvals.filter((item) => item.orgId === snapshot.activeOrgId && item.state === "pending").length,
} as Partial<Record<RouteId, number>>;
return (
<div className="app-shell">
<aside className="sidebar">
<div className="brand">
<div className="brand-mark">A</div>
<div className="brand-name">{organization?.name ?? "AgentCompany"}</div>
<Icon name="chevronDown" size={12} />
</div>
<nav className="nav">
{navItems.map((item) => (
<button
key={item.id}
type="button"
className={`nav-item ${route === item.id ? "active" : ""}`}
onClick={() => setRoute(item.id)}
>
<Icon name={item.icon} size={15} />
<span>{item.label}</span>
{badges[item.id] ? <span className="nav-badge">{badges[item.id]}</span> : null}
</button>
))}
</nav>
<div className="sidebar-foot">
<Avatar name={employee?.displayName ?? "User"} size="sm" />
<div style={{ flex: 1, minWidth: 0 }}>{employee?.displayName ?? "User"}</div>
<Icon name="bell" />
</div>
</aside>
<main className="main">
<header className="topbar">
<span className="crumb-sub">{organization?.name ?? "Workspace"}</span>
<span style={{ color: "var(--text-4)" }}>/</span>
<span className="crumb">{navItems.find((item) => item.id === route)?.label}</span>
<div className="topbar-actions">
{actions}
{window.agentCompany.platform !== "darwin" && (
<>
<IconButton icon="more" label="Minimize" onClick={() => void window.agentCompany.windowMinimize()} />
<IconButton icon="x" label="Close" onClick={() => void window.agentCompany.windowClose()} />
</>
)}
</div>
</header>
<div className="content">{children}</div>
</main>
</div>
);
}
@@ -0,0 +1,159 @@
import type { ReactNode } from "react";
import {
Bell,
Bot,
Check,
ChevronDown,
FileText,
Filter,
Folder,
Grid2X2,
Hash,
Home,
Inbox,
List,
Lock,
MessageCircle,
MoreHorizontal,
Paperclip,
Plus,
Search,
Send,
Settings,
ShieldCheck,
Sparkles,
Upload,
Users,
X,
type LucideIcon,
} from "lucide-react";
const icons: Record<string, LucideIcon> = {
approve: ShieldCheck,
attach: Paperclip,
bell: Bell,
bot: Bot,
chat: MessageCircle,
check: Check,
chevronDown: ChevronDown,
docs: FileText,
file: FileText,
filter: Filter,
folder: Folder,
grid: Grid2X2,
hash: Hash,
home: Home,
inbox: Inbox,
list: List,
lock: Lock,
more: MoreHorizontal,
plus: Plus,
search: Search,
send: Send,
settings: Settings,
social: Sparkles,
team: Users,
upload: Upload,
work: Inbox,
x: X,
};
const palette = [
["#5e6ad2", "#fff"],
["#e8b339", "#231600"],
["#26b69a", "#fff"],
["#e57373", "#fff"],
["#8b6dd9", "#fff"],
["#3a7bd5", "#fff"],
["#2a2a2e", "#fff"],
["#c2410c", "#fff"],
];
function hashName(value: string): number {
let hash = 0;
for (const char of value) hash = (hash * 31 + char.charCodeAt(0)) | 0;
return Math.abs(hash);
}
export function Icon({ name, size = 14 }: { name: string; size?: number }) {
const Component = icons[name] ?? FileText;
return <Component size={size} strokeWidth={1.7} />;
}
export function Avatar({ name, size = "", bot = false }: { name: string; size?: "sm" | ""; bot?: boolean }) {
const initials = name.split(/\s+/).map((part) => part[0]).slice(0, 2).join("").toUpperCase();
const [background, color] = palette[hashName(name) % palette.length];
return (
<div className={`avatar ${size} ${bot ? "bot" : ""}`} style={{ background, color }}>
{bot ? <Icon name="bot" size={size === "sm" ? 13 : 16} /> : initials}
</div>
);
}
export function Pill({
children,
tone,
dot = false,
}: {
children: ReactNode;
tone?: "amber" | "green" | "violet";
dot?: boolean;
}) {
return (
<span className={`pill ${tone ?? ""}`}>
{dot && <span className="dot" />}
{children}
</span>
);
}
export function Button({
children,
tone,
size,
onClick,
type = "button",
}: {
children: ReactNode;
tone?: "primary" | "ghost";
size?: "small";
onClick?: () => void;
type?: "button" | "submit";
}) {
return (
<button type={type} onClick={onClick} className={`button ${tone ?? ""} ${size ?? ""}`}>
{children}
</button>
);
}
export function IconButton({
icon,
onClick,
label,
}: {
icon: string;
onClick?: () => void;
label: string;
}) {
return (
<button type="button" className="icon-button ghost" aria-label={label} title={label} onClick={onClick}>
<Icon name={icon} />
</button>
);
}
export function Field({
label,
children,
}: {
label: string;
children: ReactNode;
}) {
return (
<label className="field">
<span>{label}</span>
{children}
</label>
);
}
@@ -0,0 +1,261 @@
:root {
--bg: #fafafa;
--panel: #ffffff;
--panel-2: #f6f6f7;
--panel-3: #f1f1f2;
--hover: #f4f4f5;
--border: #ececef;
--border-strong: #e2e2e6;
--border-soft: #f1f1f3;
--text-1: #09090b;
--text-2: #52525b;
--text-3: #a1a1aa;
--text-4: #d4d4d8;
--accent: #5e6ad2;
--accent-soft: #eef0ff;
--green: #2f9e58;
--green-soft: #e8f5ec;
--amber: #c87d12;
--amber-soft: #fbf3e2;
--red: #d23b3b;
--r-sm: 6px;
--r-md: 8px;
--r-lg: 12px;
--shadow-sm: 0 1px 0 rgba(9, 9, 11, 0.04), 0 1px 2px rgba(9, 9, 11, 0.04);
--font-sans: Inter, -apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif;
--font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
}
* { box-sizing: border-box; }
html, body, #root { width: 100%; height: 100%; margin: 0; }
body {
font-family: var(--font-sans);
color: var(--text-1);
background: var(--bg);
font-size: 13px;
letter-spacing: 0;
-webkit-font-smoothing: antialiased;
}
button, input, textarea, select { font: inherit; }
button { cursor: pointer; }
.app-shell {
display: flex;
width: 100%;
height: 100%;
overflow: hidden;
background: var(--bg);
}
.sidebar {
width: 232px;
flex-shrink: 0;
display: flex;
flex-direction: column;
padding: 14px 10px;
border-right: 1px solid var(--border-soft);
}
.brand {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: var(--r-md);
margin-bottom: 18px;
}
.brand-mark {
width: 22px;
height: 22px;
border-radius: 6px;
display: grid;
place-items: center;
color: white;
background: linear-gradient(135deg, #1a1a1d 0%, #3a3a40 100%);
font-size: 11px;
font-weight: 600;
}
.brand-name { flex: 1; font-weight: 540; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav { display: flex; flex-direction: column; gap: 1px; }
.nav-item {
display: flex;
align-items: center;
gap: 9px;
padding: 6px 8px;
border: 0;
border-radius: var(--r-md);
background: transparent;
color: var(--text-2);
font-weight: 480;
text-align: left;
}
.nav-item:hover { background: var(--hover); color: var(--text-1); }
.nav-item.active { background: var(--panel-3); color: var(--text-1); font-weight: 540; }
.nav-badge { margin-left: auto; font-size: 11px; color: var(--text-3); font-variant-numeric: tabular-nums; }
.sidebar-foot {
margin-top: auto;
padding: 10px 8px 4px;
border-top: 1px solid var(--border-soft);
display: flex;
align-items: center;
gap: 9px;
}
.main {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
background: var(--bg);
}
.topbar {
height: 44px;
flex-shrink: 0;
display: flex;
align-items: center;
gap: 12px;
padding: 0 20px;
border-bottom: 1px solid var(--border-soft);
-webkit-app-region: drag;
}
.topbar button, .topbar input, .sidebar button { -webkit-app-region: no-drag; }
.crumb { font-size: 13px; font-weight: 540; }
.crumb-sub { color: var(--text-3); }
.topbar-actions { margin-left: auto; display: flex; align-items: center; gap: 6px; }
.content { flex: 1; overflow: auto; padding: 28px 32px 40px; }
.page { max-width: 1100px; }
.page-wide { min-width: 980px; }
.page-h1 { margin: 0 0 4px; font-size: 22px; font-weight: 620; letter-spacing: 0; }
.page-sub { margin: 0 0 24px; color: var(--text-3); }
.section-label { margin-bottom: 10px; color: var(--text-3); font-size: 11px; font-weight: 540; letter-spacing: 0.02em; }
.card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--r-lg);
padding: 16px;
}
.card-label { color: var(--text-3); font-size: 11px; font-weight: 540; letter-spacing: 0.02em; }
.button {
height: 28px;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 0 10px;
border-radius: var(--r-md);
border: 1px solid var(--border-strong);
background: var(--panel);
color: var(--text-1);
font-size: 12.5px;
font-weight: 500;
}
.button:hover { background: var(--panel-2); }
.button.primary { background: var(--text-1); color: white; border-color: var(--text-1); }
.button.ghost { border-color: transparent; background: transparent; color: var(--text-2); }
.button.small { height: 24px; padding: 0 8px; font-size: 12px; }
.icon-button {
width: 28px;
height: 28px;
display: grid;
place-items: center;
border-radius: var(--r-md);
border: 1px solid var(--border);
background: var(--panel);
color: var(--text-2);
}
.icon-button.ghost { border-color: transparent; background: transparent; }
.pill {
display: inline-flex;
align-items: center;
gap: 5px;
height: 20px;
padding: 0 7px;
border-radius: 999px;
background: var(--panel-2);
color: var(--text-2);
border: 1px solid var(--border);
font-size: 11px;
font-weight: 500;
}
.pill.amber { background: var(--amber-soft); color: var(--amber); border-color: transparent; }
.pill.green { background: var(--green-soft); color: var(--green); border-color: transparent; }
.pill.violet { background: var(--accent-soft); color: var(--accent); border-color: transparent; }
.dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.avatar {
width: 28px;
height: 28px;
flex-shrink: 0;
display: grid;
place-items: center;
border-radius: 50%;
color: white;
font-size: 11px;
font-weight: 600;
}
.avatar.bot { border-radius: 8px; }
.avatar.sm { width: 22px; height: 22px; font-size: 10px; }
.grid-3 { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 12px; }
.grid-4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
.row { display: flex; align-items: center; gap: 10px; }
.num { font-variant-numeric: tabular-nums; font-family: var(--font-mono); }
.table-row {
display: grid;
gap: 12px;
align-items: center;
padding: 12px 18px;
border-bottom: 1px solid var(--border-soft);
}
.table-row:last-child { border-bottom: 0; }
.field {
display: flex;
flex-direction: column;
gap: 6px;
color: var(--text-2);
font-size: 12px;
}
.input {
width: 100%;
border: 1px solid var(--border-strong);
border-radius: var(--r-md);
background: var(--panel);
color: var(--text-1);
padding: 8px 10px;
outline: none;
}
textarea.input { min-height: 84px; resize: vertical; }
.modal-backdrop {
position: fixed;
inset: 0;
z-index: 40;
background: rgba(9, 9, 11, 0.18);
display: grid;
place-items: center;
padding: 24px;
}
.modal {
width: min(520px, 100%);
max-height: min(760px, 100%);
overflow: auto;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--panel);
box-shadow: 0 20px 60px rgba(9, 9, 11, 0.18);
padding: 18px;
}
.modal-title { margin: 0 0 16px; font-size: 16px; font-weight: 620; }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
.split-main { display: flex; flex-direction: row; min-height: 0; flex: 1; }
.secondary-pane {
width: 240px;
flex-shrink: 0;
border-right: 1px solid var(--border-soft);
padding: 14px 10px;
background: var(--bg);
}
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 4px; }
@media (max-width: 980px) {
.sidebar { width: 64px; }
.brand-name, .nav-item span:not(.nav-badge), .sidebar-foot > div { display: none; }
.content { padding: 20px 18px 32px; }
.grid-3, .grid-4 { grid-template-columns: 1fr; }
.page-wide { min-width: 0; }
.secondary-pane { width: 180px; }
}