diff --git a/apps/web/src/app.css b/apps/web/src/app.css
index c6a6c02f7..304ae68e8 100644
--- a/apps/web/src/app.css
+++ b/apps/web/src/app.css
@@ -4557,6 +4557,51 @@ tr.insights-row-clickable:hover {
height: 0.9rem;
}
+/* Bench drawer Insights and Members tabs */
+.drawer-stack {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+.drawer-link {
+ align-self: flex-start;
+ font-size: 0.85rem;
+ color: var(--foreground);
+ text-decoration: underline;
+}
+.drawer-list {
+ display: flex;
+ flex-direction: column;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+.drawer-list > li,
+.drawer-list-link {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.5rem 0;
+}
+.drawer-list-link {
+ width: 100%;
+ color: inherit;
+ text-decoration: none;
+}
+.drawer-list-text {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ font-size: 0.85rem;
+}
+.drawer-list-text > span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 0.78rem;
+ color: var(--muted-foreground);
+}
+
.stage-crumb-back {
display: inline-flex;
flex-shrink: 0;
diff --git a/apps/web/src/bench/insights-tab.tsx b/apps/web/src/bench/insights-tab.tsx
new file mode 100644
index 000000000..ab2358d1f
--- /dev/null
+++ b/apps/web/src/bench/insights-tab.tsx
@@ -0,0 +1,57 @@
+import { RichEmptyState, Skeleton } from "@corbits/react-ui";
+
+import { useAPIQuery } from "../api";
+import { insightsTopLevelRunsPath, TopLevelRunsSchema } from "../insights-api";
+import { computeBenchInsights, durationLabel, formatCount } from "../insights-stats";
+import { Link } from "../navigation";
+import { INSIGHTS_PATH_PREFIX } from "../path-ids";
+import { OutcomeChart } from "../pages/bench-insights";
+
+/** The bench Insights page in miniature: the same runs query and
+ * `computeBenchInsights`, fixed to the last 7 days. */
+export function InsightsTab({ workbenchTenantId }: { readonly workbenchTenantId: string }) {
+ const runs = useAPIQuery(insightsTopLevelRunsPath(workbenchTenantId), TopLevelRunsSchema);
+
+ if (runs.kind === "loading") return ;
+ if (runs.kind !== "ready") {
+ return (
+
+ );
+ }
+
+ const stats = computeBenchInsights(runs.data.data, 7);
+ const finished = stats.ok + stats.fail;
+ const tiles: readonly (readonly [string, string])[] = [
+ ["Runs", formatCount(stats.total)],
+ ["Succeeded", finished === 0 ? "—" : `${Math.round((stats.ok / finished) * 100)}%`],
+ ["Median run", stats.medianMs === null ? "—" : durationLabel(stats.medianMs)],
+ ];
+ const id = encodeURIComponent(workbenchTenantId);
+
+ return (
+
+
+ Last 7 days
+
+ {tiles.map(([label, value]) => (
+
+ ))}
+
+
+
+
+ Open Insights
+
+
+ );
+}
diff --git a/apps/web/src/bench/members-tab.tsx b/apps/web/src/bench/members-tab.tsx
new file mode 100644
index 000000000..101428f90
--- /dev/null
+++ b/apps/web/src/bench/members-tab.tsx
@@ -0,0 +1,80 @@
+import { RichEmptyState, Skeleton } from "@corbits/react-ui";
+import { useQuery } from "@tanstack/react-query";
+
+import { IdentityAvatar, WorkbenchAvatar } from "@/chat/avatar";
+import type { WorkbenchParticipant } from "@/chat/threads-api";
+import { Link } from "../navigation";
+import { tenantKeys } from "../query-client";
+import { principalLabel } from "../settings/identity";
+import { listPrincipals } from "../settings/tenancy-api";
+
+/** Humans are the bench tenant's user principals; agents are the bench's
+ * workers, already listed as participants. */
+export function MembersTab({
+ workbenchTenantId,
+ participants,
+}: {
+ readonly workbenchTenantId: string;
+ readonly participants: readonly WorkbenchParticipant[];
+}) {
+ // A distinct key from Settings -> People, which caches a different shape
+ // under the bare principals key; prefix invalidation still reaches both.
+ const people = useQuery({
+ queryKey: [...tenantKeys.principals(workbenchTenantId), "members"],
+ queryFn: async () => (await listPrincipals(workbenchTenantId)).filter((p) => p.kind === "user"),
+ });
+ const agents = participants.filter((p) => p.kind === "agent");
+
+ return (
+
+
+ People
+ {people.isPending ? : null}
+ {people.isError ? (
+ void people.refetch() }]}
+ />
+ ) : null}
+ {people.data?.length === 0 ? (
+ No people yet.
+ ) : null}
+
+ {(people.data ?? []).map((person) => {
+ const { label } = principalLabel(person.displayName);
+ const detail = [person.email, person.roles.map((r) => r.name).join(", ")]
+ .filter((s): s is string => s !== undefined && s !== "")
+ .join(" · ");
+ return (
+ -
+
+
+ {label}
+ {detail}
+
+
+ );
+ })}
+
+
+
+ Agents
+ {agents.length === 0 ? No agents yet.
: null}
+
+ {agents.map((agent) => (
+ -
+
+
+
+ {agent.name}
+ {agent.address === "" ? "Starting" : "Live"}
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/web/src/pages/bench-insights.tsx b/apps/web/src/pages/bench-insights.tsx
index e9385da68..fe7ec6255 100644
--- a/apps/web/src/pages/bench-insights.tsx
+++ b/apps/web/src/pages/bench-insights.tsx
@@ -37,7 +37,7 @@ function dayLabel(date: Date): string {
});
}
-function OutcomeChart({ days }: { readonly days: readonly BenchDay[] }) {
+export function OutcomeChart({ days }: { readonly days: readonly BenchDay[] }) {
const [active, setActive] = useState(null);
const max = Math.max(...days.map((d) => d.ok + d.fail), 1);
const every = days.length > 30 ? 14 : days.length > 7 ? 5 : 1;
diff --git a/apps/web/src/pages/workbench-page.tsx b/apps/web/src/pages/workbench-page.tsx
index c33eeb8be..40affe61e 100644
--- a/apps/web/src/pages/workbench-page.tsx
+++ b/apps/web/src/pages/workbench-page.tsx
@@ -27,6 +27,8 @@ import { BenchDrawer } from "../bench/bench-drawer";
import { BenchPill } from "../bench/bench-pill";
import { GrantsTab } from "../bench/grants-tab";
import { InformationTab } from "../bench/information-tab";
+import { InsightsTab } from "../bench/insights-tab";
+import { MembersTab } from "../bench/members-tab";
import { ToolsTab } from "../bench/tools-tab";
import { useBench } from "../bench-context";
import { createFetchStockHub } from "../needs-converge";
@@ -316,6 +318,13 @@ function Workbench({ workbenchTenantId }: { readonly workbenchTenantId: string }
),
Tools: ,
Grants: ,
+ Insights: ,
+ Members: (
+
+ ),
}}
/>