Purge wordy empty-state copy from renderer and tighten layout

Removes "Nothing pending.", "No tasks…", "Empty.", "No messages…", "No
files…", and similar status descriptions across Home/Work/Chat/Files/
Approvals/Social/Team/Settings panels — design-source never renders this
copy, and the user-frontend rule forbids status narration.

Lets the snapshot loader throw on IPC failure instead of holding a banner
string; the shell topbar handles its own crash boundary.

Fixes Team org-chart connector when only one agent is hired, switches
Settings to use one breadcrumb path, drops the unread badge until we have
read-state tracking, and snaps the Providers grid to the two-column
design spec.
This commit is contained in:
ZacharyZhang-NY committed 2026-05-21 20:18:14 -04:00
1 parent aa8046e56d
commit b44e0e3657
20 files changed
+262 -265

No files matched your search

@@ -12,7 +12,7 @@ import { SocialPage } from "../pages/SocialPage";
import { SettingsPage } from "../pages/SettingsPage";
export function App() {
const { snapshot, error, actions } = useSnapshot();
const { snapshot, actions } = useSnapshot();
const [route, setRoute] = useState<RouteId>("home");
if (!snapshot) {
@@ -36,11 +36,6 @@ export function App() {
return (
<Shell snapshot={snapshot} route={route} setRoute={setRoute}>
{error && (
<div className="card" style={{ marginBottom: 16, color: "var(--red)" }}>
{error}
</div>
)}
{page}
</Shell>
);
@@ -9,33 +9,23 @@ export interface SnapshotActions {
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);
}
if (!result.ok) throw new Error(result.error.message);
setSnapshot(result.data);
}, []);
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;
if (!result.ok) throw new Error(result.error.message);
if (result.data) setSnapshot(result.data as AppSnapshot);
return result.data;
}, []);
useEffect(() => {
@@ -45,5 +35,5 @@ export function useSnapshot(): {
});
}, [load]);
return { snapshot, error, actions: { run, reload: load } };
return { snapshot, actions: { run, reload: load } };
}
@@ -77,10 +77,8 @@ export function ApprovalsPage({ snapshot, actions }: PageProps) {
<StatTile label="Declined (7d)" value={String(declined7d)} />
</div>
<div className="section-label" style={{ marginBottom: 10 }}>Pending</div>
{filteredPending.length === 0 ? (
<div className="card" style={{ color: "var(--text-3)" }}>Nothing in the queue.</div>
) : filteredPending.map((approval) => (
{filteredPending.length > 0 && <div className="section-label" style={{ marginBottom: 10 }}>Pending</div>}
{filteredPending.map((approval) => (
<ApprovalRow
key={approval.id}
approval={approval}
@@ -91,20 +89,22 @@ export function ApprovalsPage({ snapshot, actions }: PageProps) {
/>
))}
<div className="section-label" style={{ margin: "32px 0 10px" }}>Recently decided</div>
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
{decided.length === 0 ? (
<div style={{ padding: 18, color: "var(--text-3)" }}>No decisions yet.</div>
) : decided.slice(0, 24).map((approval, index) => (
<DecidedRow
key={approval.id}
approval={approval}
requester={resolveRequester(approval, employees, agents)}
decider={resolveDecider(approval, employees)}
last={index === Math.min(decided.length, 24) - 1}
/>
))}
</div>
{decided.length > 0 && (
<>
<div className="section-label" style={{ margin: "32px 0 10px" }}>Recently decided</div>
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
{decided.slice(0, 24).map((approval, index) => (
<DecidedRow
key={approval.id}
approval={approval}
requester={resolveRequester(approval, employees, agents)}
decider={resolveDecider(approval, employees)}
last={index === Math.min(decided.length, 24) - 1}
/>
))}
</div>
</>
)}
</div>
);
}
@@ -230,9 +230,7 @@ function FilesTable({
>
<div>Name</div><div>Size</div><div>Owner</div><div>Modified</div><div />
</div>
{files.length === 0 ? (
<div style={{ padding: 18, color: "var(--text-3)" }}>No files here yet.</div>
) : files.map((file, index) => (
{files.map((file, index) => (
<FileRow
key={file.id}
file={file}
@@ -256,9 +254,6 @@ function FilesGrid({
agents: AgentRecord[];
onTogglePin: (file: FileObjectRecord) => void;
}) {
if (files.length === 0) {
return <div className="card" style={{ color: "var(--text-3)" }}>No files here yet.</div>;
}
return (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 12 }}>
{files.map((file) => (
@@ -98,28 +98,24 @@ function TodayCard({
<span className="card-label">Today</span>
<span style={{ marginLeft: "auto", fontSize: 11, color: "var(--text-3)" }}>{formatted}</span>
</div>
{rows.length === 0 ? (
<EmptyLine>No items on today's agenda.</EmptyLine>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{rows.map((task) => {
const owner = agents.find((agent) => agent.id === task.assigneeAgentId);
const ownerName = owner?.name ?? employee.displayName;
const time = formatTime(task.updatedAt);
const requiresApproval = approvals.some((approval) => approval.title === task.title);
return (
<div key={task.id} className="row" style={{ gap: 12 }}>
<span className="num mono" style={{ width: 36, fontSize: 12, color: "var(--text-3)" }}>{time}</span>
<div style={{ flex: 1, minWidth: 0, fontSize: 13 }}>
<span style={{ fontWeight: 500 }}>{task.title}</span>
<span style={{ color: "var(--text-3)", marginLeft: 6 }}>· {ownerName}</span>
</div>
{requiresApproval && <Pill tone="amber">You</Pill>}
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
{rows.map((task) => {
const owner = agents.find((agent) => agent.id === task.assigneeAgentId);
const ownerName = owner?.name ?? employee.displayName;
const time = formatTime(task.updatedAt);
const requiresApproval = approvals.some((approval) => approval.title === task.title);
return (
<div key={task.id} className="row" style={{ gap: 12 }}>
<span className="num mono" style={{ width: 36, fontSize: 12, color: "var(--text-3)" }}>{time}</span>
<div style={{ flex: 1, minWidth: 0, fontSize: 13 }}>
<span style={{ fontWeight: 500 }}>{task.title}</span>
<span style={{ color: "var(--text-3)", marginLeft: 6 }}>· {ownerName}</span>
</div>
);
})}
</div>
)}
{requiresApproval && <Pill tone="amber">You</Pill>}
</div>
);
})}
</div>
</section>
);
}
@@ -141,27 +137,23 @@ function WaitingCard({
<span className="card-label">Waiting on you</span>
{approvals.length > 0 && <span style={{ marginLeft: "auto" }}><Pill tone="amber">{approvals.length}</Pill></span>}
</div>
{approvals.length === 0 ? (
<EmptyLine>Nothing pending.</EmptyLine>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
{approvals.slice(0, 2).map((approval) => {
const from = resolveRequester(approval, employees, agents);
return (
<div key={approval.id} style={{ padding: 12, border: "1px solid var(--border)", borderRadius: 8 }}>
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 2 }}>{approval.title}</div>
<div style={{ fontSize: 12, color: "var(--text-3)", marginBottom: 10 }}>
{approval.kind} · {approval.risk} risk · from {from}
</div>
<div className="row" style={{ gap: 6 }}>
<Button tone="primary" size="small" onClick={() => onApprove(approval.id)}>Approve</Button>
<Button size="small">Open</Button>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
{approvals.slice(0, 2).map((approval) => {
const from = resolveRequester(approval, employees, agents);
return (
<div key={approval.id} style={{ padding: 12, border: "1px solid var(--border)", borderRadius: 8 }}>
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 2 }}>{approval.title}</div>
<div style={{ fontSize: 12, color: "var(--text-3)", marginBottom: 10 }}>
{approval.kind} · {approval.risk} risk · from {from}
</div>
);
})}
</div>
)}
<div className="row" style={{ gap: 6 }}>
<Button tone="primary" size="small" onClick={() => onApprove(approval.id)}>Approve</Button>
<Button size="small">Open</Button>
</div>
</div>
);
})}
</div>
</section>
);
}
@@ -188,23 +180,19 @@ function SpendCard({
<div style={{ marginTop: 14, height: 4, background: "var(--panel-3)", borderRadius: 2, overflow: "hidden" }}>
<div style={{ width: `${fillPct}%`, height: "100%", background: "var(--text-1)" }} />
</div>
{connected.length === 0 ? (
<EmptyLine style={{ marginTop: 14 }}>No providers connected.</EmptyLine>
) : (
<div style={{ marginTop: 14, display: "flex", flexDirection: "column", gap: 8 }}>
{connected.slice(0, 3).map((provider) => {
const usingAgents = agents.filter((agent) => agent.modelProviderId === provider.id).length;
return (
<div key={provider.id} className="row" style={{ fontSize: 12 }}>
<span style={{ color: "var(--text-2)" }}>{provider.label}</span>
<span className="num mono" style={{ marginLeft: "auto", color: "var(--text-1)", fontWeight: 500 }}>
{usingAgents} {usingAgents === 1 ? "agent" : "agents"}
</span>
</div>
);
})}
</div>
)}
<div style={{ marginTop: 14, display: "flex", flexDirection: "column", gap: 8 }}>
{connected.slice(0, 3).map((provider) => {
const usingAgents = agents.filter((agent) => agent.modelProviderId === provider.id).length;
return (
<div key={provider.id} className="row" style={{ fontSize: 12 }}>
<span style={{ color: "var(--text-2)" }}>{provider.label}</span>
<span className="num mono" style={{ marginLeft: "auto", color: "var(--text-1)", fontWeight: 500 }}>
{usingAgents} {usingAgents === 1 ? "agent" : "agents"}
</span>
</div>
);
})}
</div>
</section>
);
}
@@ -217,9 +205,7 @@ function InProgressCard({ tasks, agents }: { tasks: TaskRecord[]; agents: AgentR
<span className="card-label">In progress</span>
<span style={{ marginLeft: "auto", fontSize: 11, color: "var(--text-3)" }}>{tasks.length} active</span>
</div>
{rows.length === 0 ? (
<EmptyLine>No tasks in progress.</EmptyLine>
) : rows.map((task, index) => {
{rows.map((task, index) => {
const owner = agents.find((agent) => agent.id === task.assigneeAgentId);
const last = index === rows.length - 1;
return (
@@ -293,12 +279,6 @@ function GoalCard({
);
}
function EmptyLine({ children, style }: { children: string; style?: React.CSSProperties }) {
return (
<div style={{ color: "var(--text-3)", fontSize: 12.5, padding: "4px 0", ...style }}>{children}</div>
);
}
function collectAgendaTasks(tasks: TaskRecord[], reference: Date): TaskRecord[] {
const startOfDay = new Date(reference.getFullYear(), reference.getMonth(), reference.getDate()).getTime();
const endOfDay = startOfDay + 24 * 60 * 60 * 1000;
@@ -47,20 +47,6 @@ const ACCOUNT_GROUP: NavSpec[] = [
{ id: "security", label: "Security" },
];
const TAB_TITLES: Record<SettingsTab, string> = {
workspace: "Workspace",
members: "Members",
billing: "Billing",
models: "Models",
providers: "Providers",
skills: "Skills",
policies: "Policies",
migration: "Migration",
profile: "Profile",
notifications: "Notifications",
security: "Security",
};
export function SettingsPage({ snapshot, actions }: PageProps) {
const orgId = activeOrgId(snapshot);
const organization = requireActiveOrganization(snapshot);
@@ -93,11 +79,6 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
<NavGroup items={ACCOUNT_GROUP} active={tab} onChange={setTab} />
</aside>
<div className="main" style={{ flex: 1, minWidth: 0 }}>
<header className="topbar" style={{ borderBottom: "1px solid var(--border-soft)" }}>
<span className="crumb-sub">Settings</span>
<span className="crumb-sep">/</span>
<span className="crumb">{TAB_TITLES[tab]}</span>
</header>
<div className="content">
{tab === "workspace" && <WorkspacePanel organization={organization} workspaces={workspaces} />}
{tab === "members" && <MembersPanel employees={employees} agents={agents} />}
@@ -82,26 +82,26 @@ export function SocialPage({ snapshot, actions }: PageProps) {
</div>
</div>
<div className="section-label" style={{ marginBottom: 10 }}>Accounts</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 10, marginBottom: 28 }}>
{accounts.length === 0 ? (
<div className="card" style={{ color: "var(--text-3)" }}>No social accounts connected.</div>
) : accounts.map((account) => (
<AccountCard
key={account.id}
account={account}
authorizedName={account.authorizedEmployeeId ? employees.find((item) => item.id === account.authorizedEmployeeId)?.displayName ?? null : null}
onSignIn={() => void actions.run(() => window.agentCompany.launchSocialLogin({ socialAccountId: account.id }))}
/>
))}
</div>
{accounts.length > 0 && (
<>
<div className="section-label" style={{ marginBottom: 10 }}>Accounts</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))", gap: 10, marginBottom: 28 }}>
{accounts.map((account) => (
<AccountCard
key={account.id}
account={account}
authorizedName={account.authorizedEmployeeId ? employees.find((item) => item.id === account.authorizedEmployeeId)?.displayName ?? null : null}
onSignIn={() => void actions.run(() => window.agentCompany.launchSocialLogin({ socialAccountId: account.id }))}
/>
))}
</div>
</>
)}
<div style={{ display: "grid", gridTemplateColumns: "minmax(0, 1.4fr) minmax(0, 1fr)", gap: 12, alignItems: "start" }}>
<div>
<div className="section-label" style={{ marginBottom: 10 }}>Scheduled</div>
{scheduled.length === 0 ? (
<div className="card" style={{ color: "var(--text-3)", marginBottom: 24 }}>No posts scheduled.</div>
) : scheduled.map((post) => (
{scheduled.length > 0 && <div className="section-label" style={{ marginBottom: 10 }}>Scheduled</div>}
{scheduled.map((post) => (
<PostRow
key={post.id}
post={post}
@@ -111,11 +111,8 @@ export function SocialPage({ snapshot, actions }: PageProps) {
onPublish={() => updatePostState(post, "published")}
/>
))}
<div className="section-label" style={{ margin: "24px 0 10px" }}>Drafts</div>
{drafts.length === 0 ? (
<div className="card" style={{ color: "var(--text-3)" }}>No drafts.</div>
) : drafts.map((post) => (
{drafts.length > 0 && <div className="section-label" style={{ margin: "24px 0 10px" }}>Drafts</div>}
{drafts.map((post) => (
<PostRow
key={post.id}
post={post}
@@ -74,16 +74,19 @@ export function TeamPage({ snapshot, actions }: PageProps) {
</div>
) : null}
{agents.length > 0 && <OrgConnector slots={agents.length} />}
<div style={{ display: "grid", gridTemplateColumns: `repeat(${Math.max(1, Math.min(4, agents.length))}, 1fr)`, gap: 14, marginTop: 18 }}>
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: 14,
marginTop: 18,
justifyContent: "center",
}}
>
{agents.slice(0, 4).map((agent) => (
<OrgCard key={agent.id} name={agent.name} role={agent.role} />
))}
</div>
{agents.length > 4 && (
<div style={{ marginTop: 16, color: "var(--text-3)", fontSize: 12, textAlign: "center" }}>
+ {agents.length - 4} more agents in the roster below
</div>
)}
</section>
<div className="section-label" style={{ marginBottom: 10 }}>Roster</div>
@@ -94,9 +97,7 @@ export function TeamPage({ snapshot, actions }: PageProps) {
>
<div>Member</div><div>Department</div><div>Model</div><div>Budget</div><div />
</div>
{rosterRows.length === 0 ? (
<div style={{ padding: 18, color: "var(--text-3)" }}>No members for this filter.</div>
) : rosterRows.map((row, index) => (
{rosterRows.map((row, index) => (
<RosterRow key={row.id} row={row} last={index === rosterRows.length - 1} />
))}
</div>
@@ -156,15 +157,22 @@ function OrgCard({ name, role, human = false }: { name: string; role: string; hu
}
function OrgConnector({ slots }: { slots: number }) {
const count = Math.max(1, Math.min(4, slots));
if (slots <= 1) {
return (
<svg width="100%" height="36" style={{ display: "block", marginTop: 18 }} aria-hidden="true">
<line x1="50%" x2="50%" y1="0" y2="36" stroke="var(--border-strong)" strokeWidth="1" />
</svg>
);
}
const count = Math.min(4, slots);
const step = 100 / (count * 2);
const positions = Array.from({ length: count }, (_, i) => step * (2 * i + 1));
const left = positions[0];
const right = positions[positions.length - 1];
return (
<svg width="100%" height="36" style={{ display: "block", marginTop: 18 }} aria-hidden="true">
<line x1="50%" x2="50%" y1="0" y2="14" stroke="var(--border-strong)" strokeWidth="1" />
{count > 1 && (
<line x1={`${positions[0]}%`} x2={`${positions[positions.length - 1]}%`} y1="14" y2="14" stroke="var(--border-strong)" strokeWidth="1" />
)}
<line x1={`${left}%`} x2={`${right}%`} y1="14" y2="14" stroke="var(--border-strong)" strokeWidth="1" />
{positions.map((pos) => (
<line key={pos} x1={`${pos}%`} x2={`${pos}%`} y1="14" y2="36" stroke="var(--border-strong)" strokeWidth="1" />
))}
@@ -120,9 +120,6 @@ function BoardView({
onAdvance={() => onAdvance(task)}
/>
))}
{columnTasks.length === 0 && (
<div style={{ color: "var(--text-3)", fontSize: 12, padding: "12px 6px" }}>Empty.</div>
)}
</div>
);
})}
@@ -139,9 +136,7 @@ function ListView({ tasks, agents, projects }: { tasks: TaskRecord[]; agents: Ag
>
<div>Title</div><div>Status</div><div>Priority</div><div>Assignee</div><div />
</div>
{tasks.length === 0 ? (
<div style={{ padding: 18, color: "var(--text-3)" }}>No tasks.</div>
) : tasks.map((task, index) => {
{tasks.map((task, index) => {
const assignee = agents.find((agent) => agent.id === task.assigneeAgentId) ?? null;
const project = projects.find((p) => p.id === task.projectId) ?? null;
const last = index === tasks.length - 1;
@@ -182,9 +177,7 @@ function TimelineView({ tasks, goals }: { tasks: TaskRecord[]; goals: GoalRecord
return (
<div className="card" style={{ padding: 18 }}>
<div className="section-label">By goal</div>
{goals.length === 0 ? (
<div style={{ color: "var(--text-3)" }}>No goals defined.</div>
) : goals.map((goal) => {
{goals.map((goal) => {
const linked = tasks.filter((task) => task.goalId === goal.id);
const done = linked.filter((task) => task.status === TASK_STATUS.DONE).length;
const pct = linked.length === 0 ? 0 : Math.round((done / linked.length) * 100);
@@ -1,4 +1,4 @@
import type { FormEvent, MutableRefObject, ReactNode } from "react";
import type { FormEvent, MutableRefObject } from "react";
import { AvatarStack, Icon, IconButton } from "../../shared/ui";
import type {
ApprovalRecord,
@@ -51,11 +51,7 @@ export function ChatConversation({
<section style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", background: "var(--panel)" }}>
{space && <ConversationHeader space={space} participants={participants} />}
<div style={{ flex: 1, overflow: "auto", padding: "20px 24px 8px" }}>
{!space ? (
<Empty>Select a channel to start.</Empty>
) : rootMessages.length === 0 && pendingApprovals.length === 0 ? (
<Empty>No messages in {space.name} yet.</Empty>
) : (
{space && (
<ChatTimeline
rootMessages={rootMessages}
messagesInSpace={messagesInSpace}
@@ -119,6 +115,3 @@ function ConversationHeader({
);
}
function Empty({ children }: { children: ReactNode }) {
return <div style={{ color: "var(--text-3)", padding: 12 }}>{children}</div>;
}
@@ -34,21 +34,21 @@ export function NotificationsPanel({ events, employee }: { events: AuditEventRec
<div className="page">
<h1 className="page-h1">Notifications</h1>
<p className="page-sub">Real audit events flagged for you.</p>
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
{visible.length === 0 ? (
<div style={{ padding: 18, color: "var(--text-3)" }}>Quiet inbox.</div>
) : visible.map((event, index) => (
<div
key={event.id}
className="row"
style={{ padding: "12px 18px", borderBottom: index === visible.length - 1 ? "none" : "1px solid var(--border-soft)" }}
>
<Pill>{event.entityType.replaceAll("_", " ")}</Pill>
<span style={{ flex: 1, fontWeight: 480, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{event.action}</span>
<span style={{ color: "var(--text-3)", fontSize: 12 }}>{formatRelative(event.createdAt)}</span>
</div>
))}
</div>
{visible.length > 0 && (
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
{visible.map((event, index) => (
<div
key={event.id}
className="row"
style={{ padding: "12px 18px", borderBottom: index === visible.length - 1 ? "none" : "1px solid var(--border-soft)" }}
>
<Pill>{event.entityType.replaceAll("_", " ")}</Pill>
<span style={{ flex: 1, fontWeight: 480, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{event.action}</span>
<span style={{ color: "var(--text-3)", fontSize: 12 }}>{formatRelative(event.createdAt)}</span>
</div>
))}
</div>
)}
</div>
);
}
@@ -59,21 +59,21 @@ export function SecurityPanel({ events }: { events: AuditEventRecord[] }) {
<div className="page">
<h1 className="page-h1">Security</h1>
<p className="page-sub">Recent security-relevant audit events.</p>
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
{security.length === 0 ? (
<div style={{ padding: 18, color: "var(--text-3)" }}>No security events recorded.</div>
) : security.map((event, index) => (
<div
key={event.id}
className="row"
style={{ padding: "12px 18px", borderBottom: index === security.length - 1 ? "none" : "1px solid var(--border-soft)" }}
>
<Pill tone="amber">{event.entityType.replaceAll("_", " ")}</Pill>
<span style={{ flex: 1, fontWeight: 480, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{event.action}</span>
<span style={{ color: "var(--text-3)", fontSize: 12 }}>{formatRelative(event.createdAt)}</span>
</div>
))}
</div>
{security.length > 0 && (
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
{security.map((event, index) => (
<div
key={event.id}
className="row"
style={{ padding: "12px 18px", borderBottom: index === security.length - 1 ? "none" : "1px solid var(--border-soft)" }}
>
<Pill tone="amber">{event.entityType.replaceAll("_", " ")}</Pill>
<span style={{ flex: 1, fontWeight: 480, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{event.action}</span>
<span style={{ color: "var(--text-3)", fontSize: 12 }}>{formatRelative(event.createdAt)}</span>
</div>
))}
</div>
)}
</div>
);
}
@@ -31,9 +31,7 @@ export function BillingPanel({
>
<div>Provider</div><div>Requests</div><div>Errors</div><div>Agents</div>
</div>
{providers.length === 0 ? (
<div style={{ padding: 18, color: "var(--text-3)" }}>No providers configured.</div>
) : providers.map((provider, index) => {
{providers.map((provider, index) => {
const requests = monthRequests.filter((request) => request.providerId === provider.id);
const errors = requests.filter((request) => request.state === "failed").length;
const boundAgents = agents.filter((agent) => agent.modelProviderId === provider.id).length;
@@ -46,7 +44,9 @@ export function BillingPanel({
>
<div style={{ minWidth: 0 }}>
<div style={{ fontWeight: 540 }}>{provider.label}</div>
<div style={{ fontSize: 11, color: "var(--text-3)" }}>{provider.model ?? "no default model"}</div>
{provider.model && (
<div style={{ fontSize: 11, color: "var(--text-3)" }}>{provider.model}</div>
)}
</div>
<div className="num mono">{requests.length}</div>
<div>{errors > 0 ? <Pill tone="amber">{errors}</Pill> : <span className="num mono" style={{ color: "var(--text-3)" }}>0</span>}</div>
@@ -44,13 +44,13 @@ export function MigrationPanel({
<Button tone="primary" onClick={() => setOpen(true)}><Icon name="plus" size={13} /> Report</Button>
</div>
</div>
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
{reports.length === 0 ? (
<div style={{ padding: 18, color: "var(--text-3)" }}>No migration reports yet.</div>
) : reports.map((report, index) => (
<ReportRow key={report.id} report={report} last={index === reports.length - 1} />
))}
</div>
{reports.length > 0 && (
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
{reports.map((report, index) => (
<ReportRow key={report.id} report={report} last={index === reports.length - 1} />
))}
</div>
)}
<Dialog title="Migration report" open={open} submitLabel="Create" onClose={() => setOpen(false)} onSubmit={submit}>
<Field label="Source database path"><input className="input" name="sourceDatabasePath" required /></Field>
</Dialog>
@@ -20,9 +20,7 @@ export function ModelsPanel({
>
<div>Provider</div><div>Model</div><div>Agents</div><div>Source</div>
</div>
{rows.length === 0 ? (
<div style={{ padding: 18, color: "var(--text-3)" }}>No models configured.</div>
) : rows.map((row, index) => (
{rows.map((row, index) => (
<div
key={`${row.providerId}-${row.model}`}
className="table-row"
@@ -13,9 +13,7 @@ export function PoliciesPanel({ policies }: { policies: PolicyRuleRecord[] }) {
>
<div>Action</div><div>Effect</div><div>Risk ceiling</div><div>Subject</div>
</div>
{policies.length === 0 ? (
<div style={{ padding: 18, color: "var(--text-3)" }}>No policies.</div>
) : policies.map((policy, index) => (
{policies.map((policy, index) => (
<div
key={policy.id}
className="table-row"
@@ -50,15 +50,15 @@ export function ProvidersPanel({ orgId, providers, agents, actions }: ProvidersP
</>
)}
<div className="section-label" style={{ marginBottom: 10 }}>Available</div>
{available.length === 0 ? (
<div className="card" style={{ color: "var(--text-3)" }}>All providers connected.</div>
) : (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(260px, 1fr))", gap: 10 }}>
{available.map((provider) => (
<ProviderCard key={provider.id} provider={provider} onConnect={() => setActive(provider)} />
))}
</div>
{available.length > 0 && (
<>
<div className="section-label" style={{ marginBottom: 10 }}>Available</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: 10 }}>
{available.map((provider) => (
<ProviderCard key={provider.id} provider={provider} onConnect={() => setActive(provider)} />
))}
</div>
</>
)}
<Dialog title="Provider credential" open={Boolean(active)} submitLabel="Save" onClose={() => setActive(null)} onSubmit={save}>
@@ -58,9 +58,7 @@ export function SkillsPanel({ orgId, skills, revisions, bindings, agents, action
>
<div>Skill</div><div>Version</div><div>Bindings</div><div>Bind to agent</div>
</div>
{skills.length === 0 ? (
<div style={{ padding: 18, color: "var(--text-3)" }}>No skills imported yet.</div>
) : skills.map((skill, index) => {
{skills.map((skill, index) => {
const last = index === skills.length - 1;
const latest = revisions.find((revision) => revision.skillId === skill.id);
const skillBindings = bindings.filter((binding) => binding.skillId === skill.id && binding.enabled);
@@ -76,9 +74,7 @@ export function SkillsPanel({ orgId, skills, revisions, bindings, agents, action
</div>
<Pill>{latest ? latest.versionSemver : "—"}</Pill>
<div className="row" style={{ gap: 6, flexWrap: "wrap" }}>
{skillBindings.length === 0 ? (
<span style={{ color: "var(--text-3)", fontSize: 12 }}>Not bound</span>
) : skillBindings.map((binding) => (
{skillBindings.map((binding) => (
<Pill key={binding.id}>{agentName(binding.agentId, agents)}</Pill>
))}
</div>
@@ -107,7 +103,7 @@ function BindAgent({
onBind: (agentId: string) => void;
disabled: boolean;
}) {
if (agents.length === 0) return <span style={{ color: "var(--text-3)", fontSize: 12 }}>No agents available</span>;
if (agents.length === 0) return null;
return (
<select
className="input"
@@ -30,22 +30,22 @@ export function WorkspacePanel({
</div>
</div>
<div className="card" style={{ padding: 18 }}>
<div className="section-label" style={{ marginBottom: 12 }}>Workspaces</div>
{workspaces.length === 0 ? (
<div style={{ color: "var(--text-3)" }}>No workspaces.</div>
) : workspaces.map((workspace) => (
<div key={workspace.id} className="row" style={{ padding: "8px 0", borderBottom: "1px solid var(--border-soft)" }}>
<div style={{ minWidth: 0 }}>
<div style={{ fontWeight: 540 }}>{workspace.name}</div>
<div style={{ fontSize: 12, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{workspace.localPath}</div>
{workspaces.length > 0 && (
<div className="card" style={{ padding: 18 }}>
<div className="section-label" style={{ marginBottom: 12 }}>Workspaces</div>
{workspaces.map((workspace) => (
<div key={workspace.id} className="row" style={{ padding: "8px 0", borderBottom: "1px solid var(--border-soft)" }}>
<div style={{ minWidth: 0 }}>
<div style={{ fontWeight: 540 }}>{workspace.name}</div>
<div style={{ fontSize: 12, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{workspace.localPath}</div>
</div>
<div style={{ marginLeft: "auto" }}>
{workspace.isPrimary ? <Pill tone="violet">Primary</Pill> : <Pill>Secondary</Pill>}
</div>
</div>
<div style={{ marginLeft: "auto" }}>
{workspace.isPrimary ? <Pill tone="violet">Primary</Pill> : <Pill>Secondary</Pill>}
</div>
</div>
))}
</div>
))}
</div>
)}
</div>
);
}
@@ -122,6 +122,7 @@ export function Shell({
);
}
function labelFor(route: RouteId): string {
const spec = primaryNav.find((item) => item.id === route);
if (spec) return spec.label;
@@ -136,11 +137,9 @@ function renderBadge(value: number | undefined): ReactNode {
function computeBadges(snapshot: AppSnapshot, orgId: string): Partial<Record<RouteId, number>> {
const tasks = snapshot.tasks.filter((item) => item.orgId === orgId);
const messages = snapshot.chatMessages.filter((item) => item.orgId === orgId);
const approvals = snapshot.approvals.filter((item) => item.orgId === orgId);
return {
work: tasks.filter((task) => task.status !== "done").length,
chat: messages.length,
approvals: approvals.filter((approval) => approval.state === "pending").length,
};
}
@@ -0,0 +1,74 @@
import { _electron as electron, expect, test, type Page } from "@playwright/test";
import { mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { randomUUID } from "node:crypto";
test("captures every page for visual review", async () => {
const profileDir = join(tmpdir(), `agentcompany-new-shots-${randomUUID()}`);
mkdirSync(profileDir, { recursive: true });
const app = await electron.launch({
args: [resolve("out/main/index.js")],
env: { ...process.env, AGENTCOMPANY_NEW_PROFILE_DIR: profileDir },
});
const outDir = resolve("test-results/screenshots");
mkdirSync(outDir, { recursive: true });
try {
const page = await app.firstWindow();
await page.setViewportSize({ width: 1440, height: 900 });
await page.screenshot({ path: join(outDir, "00-onboarding-welcome.png"), fullPage: false });
await page.getByRole("button", { name: "Get started" }).click();
await page.screenshot({ path: join(outDir, "01-onboarding-provider.png"), fullPage: false });
await page.getByLabel("API key").fill("local-dev-key");
await page.getByRole("button", { name: "Continue" }).click();
await page.getByLabel("Company name").fill("OrbitOps");
await page.getByLabel("Your name").fill("Yu Chen");
await page.getByLabel("Description").fill("Multi-agent operations platform.");
await page.screenshot({ path: join(outDir, "02-onboarding-company.png"), fullPage: false });
await page.getByRole("button", { name: "Continue" }).click();
await page.screenshot({ path: join(outDir, "03-onboarding-mode.png"), fullPage: false });
await page.getByRole("button", { name: "Continue" }).click();
await page.getByLabel("Goal").fill("Ship the v1 launch by July.");
await page.getByLabel("Acceptance criteria").fill("Pricing model + landing page + 3 pilot signups.");
await page.screenshot({ path: join(outDir, "04-onboarding-goal.png"), fullPage: false });
await page.getByRole("button", { name: "Launch" }).click();
await expect(page.getByRole("button", { name: /OrbitOps/ })).toBeVisible();
await snap(page, outDir, "10-home");
await go(page, "Team", outDir, "11-team");
await go(page, "Work", outDir, "12-work");
await go(page, "Messages", outDir, "13-messages");
await go(page, "Files", outDir, "14-files");
await go(page, "Approvals", outDir, "15-approvals");
await go(page, "Social", outDir, "16-social");
await go(page, "Settings", outDir, "17-settings");
for (const tab of ["Workspace", "Members", "Billing", "Models", "Skills", "Policies", "Migration", "Profile", "Notifications", "Security"]) {
await page.getByRole("button", { name: "Settings" }).click();
await page.getByRole("button", { name: tab }).first().click();
await snap(page, outDir, `settings-${tab.toLowerCase()}`);
}
await page.setViewportSize({ width: 980, height: 700 });
await go(page, "Home", outDir, "20-narrow-home");
await go(page, "Messages", outDir, "21-narrow-messages");
await go(page, "Files", outDir, "22-narrow-files");
} finally {
await app.close();
}
});
async function go(page: Page, nav: string, dir: string, name: string): Promise<void> {
await page.getByRole("button", { name: nav }).click();
await snap(page, dir, name);
}
async function snap(page: Page, dir: string, name: string): Promise<void> {
await page.waitForTimeout(120);
await page.screenshot({ path: join(dir, `${name}.png`), fullPage: false });
}