Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/bench/artifacts-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function ArtifactsTab({ workbenchTenantId }: { readonly workbenchTenantId
<section className="drawer-sec">
<div className="drawer-sec-head">
<h3>Files saved here</h3>
<Link to={benchLink(ARTIFACTS_PATH_PREFIX, workbenchTenantId)}>See all</Link>
<Link to={benchLink(ARTIFACTS_PATH_PREFIX, workbenchTenantId)}>Open all</Link>
</div>
{page.kind === "loading" ? <Skeleton className="h-16 w-full" /> : null}
{page.kind === "error" ? <p className="workbench-info-empty-note">{page.message}</p> : null}
Expand Down
21 changes: 16 additions & 5 deletions apps/web/src/bench/bench-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Button } from "@corbits/react-ui";
import "./drawer-narrow.css";
import { X } from "@/lib/icons";
import "./drawer.css";
import { useEffect, useRef, useState, type ReactNode } from "react";
import { createContext, useEffect, useRef, useState, type ReactNode } from "react";

export const DRAWER_TABS = [
"Information",
Expand All @@ -16,6 +16,9 @@ export const DRAWER_TABS = [

export type DrawerTab = (typeof DRAWER_TABS)[number];

/** Lets a tab body switch the drawer to another tab. */
export const DrawerTabContext = createContext<(tab: DrawerTab) => void>(() => undefined);

/** The floating card in the grid column that pushes the thread left. Each
* tab's body arrives as a prop keyed by tab name; a tab without one shows a
* short empty state until its own ticket lands. */
Expand Down Expand Up @@ -99,7 +102,13 @@ export function BenchDrawer({
<h2>{title}</h2>
{subtitle === undefined || subtitle === "" ? null : <p>{subtitle}</p>}
</div>
<Button variant="ghost" size="sm" aria-label="Close drawer" onClick={onClose}>
<Button
variant="ghost"
size="sm"
className="drawer-close"
aria-label="Close drawer"
onClick={onClose}
>
<X size={16} aria-hidden="true" />
</Button>
</div>
Expand All @@ -118,9 +127,11 @@ export function BenchDrawer({
))}
</div>
<div className="drawer-body" role="tabpanel">
{body ?? (
<p className="workbench-info-empty-note">{tab} is coming to this drawer soon.</p>
)}
<DrawerTabContext.Provider value={setTab}>
{body ?? (
<p className="workbench-info-empty-note">{tab} is coming to this drawer soon.</p>
)}
</DrawerTabContext.Provider>
</div>
</div>
</div>
Expand Down
67 changes: 66 additions & 1 deletion apps/web/src/bench/drawer.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,74 @@
.drawer-grant-mode[data-effect="deny"] {
color: var(--ink-2);
}
.drawer .bi-stats {
grid-template-columns: 1fr 1fr;
}
.drawer .bi-stat {
padding: 12px 14px;
}
.drawer .bi-stat + .bi-stat {
border-left: 0;
}
.drawer .bi-stat:nth-child(even) {
border-left: 1px solid var(--line);
}
.drawer .bi-stat:nth-child(n + 3) {
border-top: 1px solid var(--line);
}
.drawer .bi-stat-v {
font-size: 20px;
letter-spacing: -0.01em;
}
.drawer .bi-chart {
margin-top: 0;
}

.drawer .drawer-close {
width: 32px;
height: 32px;
padding: 0;
flex: 0 0 auto;
}
.drawer-stack {
display: flex;
}
.drawer-stack > * + * {
margin-left: -6px;
}
.drawer-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
color: var(--ink-2);
background: color-mix(in srgb, var(--action) 14%, transparent);
}
.drawer-pill::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--action);
}
.drawer-sec-head button {
padding: 0;
border: 0;
border-radius: 0;
background: none;
cursor: pointer;
font-size: 12.5px;
color: var(--ink-3);
}
.drawer-sec-head button:hover {
color: var(--ink);
}
.drawer .workbench-info-panel-header button {
height: auto;
padding: 0;
font-size: 12.5px;
color: var(--ink-3);
}
103 changes: 93 additions & 10 deletions apps/web/src/bench/information-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Button, Skeleton, formatRelativeTime } from "@corbits/react-ui";
import { toast } from "@corbits/react-ui/ui/toast";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { useContext, useState } from "react";

import { ArtifactListPageSchema, useAPIQuery } from "@/api";
import { ApprovalRow } from "@/chat/approval-row";
import { IdentityAvatar } from "@/chat/avatar";
import type { WorkbenchParticipant } from "@/chat/threads-api";
import { libraryArtifactPath } from "@/library";
Expand All @@ -17,28 +16,39 @@ import { scheduleSentence } from "@/pages/routines-page";
import { listScheduledWorkflows } from "@/routines-api";
import { skillDisplayName } from "@/skill-display-name";
import { listSkills } from "@/skills-api";
import { principalLabel } from "../settings/identity";
import { listPrincipals } from "../settings/tenancy-api";
import { WorkbenchSchedulesPanel } from "../pages/workbench-schedules-panel";
import { usePendingApprovals } from "../pending-approvals";
import { benchLink } from "../shell/page-crumbs";
import type { WorkerStatus } from "../worker-status";
import { DrawerTabContext } from "./bench-drawer";
import "./description.css";
import { DESCRIPTION_MAX, useBenchDescription } from "./description";
import { DEFAULT_WORKER_NAME, WORKER_NAME_MAX, useWorkerName } from "./worker-name";

function Section({
title,
action,
onAction,
children,
}: {
readonly title: string;
readonly action?: { readonly to: string; readonly label: string };
/** A head button, for an action that stays in the drawer. */
readonly onAction?: { readonly onClick: () => void; readonly label: string };
readonly children: React.ReactNode;
}) {
return (
<section className="drawer-sec">
<div className="drawer-sec-head">
<h3>{title}</h3>
{action === undefined ? null : <Link to={action.to}>{action.label}</Link>}
{onAction === undefined ? null : (
<button type="button" onClick={onAction.onClick}>
{onAction.label}
</button>
)}
</div>
{children}
</section>
Expand All @@ -48,15 +58,28 @@ function Section({
function AboutSection({ workbenchTenantId }: { readonly workbenchTenantId: string }) {
const { description, save } = useBenchDescription(workbenchTenantId);
const [draft, setDraft] = useState<string | null>(null);
const value = draft ?? description;
const editing = draft !== null;
const dirty = draft !== null && draft.trim() !== description;
if (!editing) {
return (
<Section
title="About"
onAction={{
label: description === "" ? "Add" : "Edit",
onClick: () => setDraft(description),
}}
>
<p className="drawer-about">{description === "" ? "No description yet." : description}</p>
</Section>
);
}
return (
<Section title="About">
<Section title="About" onAction={{ label: "Cancel", onClick: () => setDraft(null) }}>
<form
className="bench-description-form"
onSubmit={(event) => {
event.preventDefault();
save.mutate(value.trim(), {
save.mutate(draft.trim(), {
onSuccess: () => setDraft(null),
onError: (cause) => toast(cause instanceof Error ? cause.message : String(cause)),
});
Expand All @@ -66,22 +89,66 @@ function AboutSection({ workbenchTenantId }: { readonly workbenchTenantId: strin
aria-label="Description"
placeholder="Add a description"
maxLength={DESCRIPTION_MAX}
value={value}
value={draft}
autoFocus
onChange={(event) => setDraft(event.target.value)}
/>
<div className="bench-description-actions">
<Button type="submit" size="sm" disabled={!dirty || save.isPending}>
Save
</Button>
<span className="bench-description-count">
{value.length}/{DESCRIPTION_MAX}
{draft.length}/{DESCRIPTION_MAX}
</span>
</div>
</form>
</Section>
);
}

function MembersSummary({
workbenchTenantId,
participants,
}: {
readonly workbenchTenantId: string;
readonly participants: readonly WorkbenchParticipant[];
}) {
const setTab = useContext(DrawerTabContext);
// Same key as the Members tab so the two share one fetch.
const people = useQuery({
queryKey: [...tenantKeys.principals(workbenchTenantId), "members"],
queryFn: async () => (await listPrincipals(workbenchTenantId)).filter((p) => p.kind === "user"),
});
const workers = participants.filter((p) => p.kind === "agent");
const humans = people.data ?? [];
const workersText = `${workers.length} worker${workers.length === 1 ? "" : "s"}`;
const peopleText = humans.length === 1 ? "1 person" : `${humans.length} people`;
return (
<Section title="Members" onAction={{ label: "Manage", onClick: () => setTab("Members") }}>
<div className="drawer-li" style={{ cursor: "default" }}>
<span className="drawer-stack">
{workers.map((w) => (
<IdentityAvatar key={w.id} kind="agent" name={w.name} principalId={w.id} />
))}
{humans.map((p) => (
<IdentityAvatar
key={p.id}
kind="person"
name={principalLabel(p.displayName).label}
principalId={p.id}
/>
))}
</span>
<span className="drawer-li-t">
<span>
{workersText}, {peopleText}
</span>
</span>
</div>
</Section>
);
}

function WorkerNameSection({ workbenchTenantId }: { readonly workbenchTenantId: string }) {
const { name, save } = useWorkerName(workbenchTenantId);
const current = name ?? DEFAULT_WORKER_NAME;
Expand Down Expand Up @@ -188,11 +255,25 @@ export function InformationTab({
<p className="workbench-info-empty-note">Nothing is waiting on you.</p>
) : null}
{pendingApprovals !== null && pendingApprovals.length > 0 ? (
<ul className="workbench-info-approval-list">
<div className="drawer-list">
{pendingApprovals.map((item) => (
<ApprovalRow key={item.id} item={item} tenantId={workbenchTenantId} />
<div key={item.id} className="drawer-li">
<IdentityAvatar
kind="agent"
name={item.agentName}
principalId={item.agentAddress}
/>
<span className="drawer-li-t">
<b>{item.headline}</b>
<span>
{item.agentName}
{item.toolName === undefined ? "" : ` · ${item.toolName}`}
</span>
</span>
<span className="drawer-pill">Waiting</span>
</div>
))}
</ul>
</div>
) : null}
</Section>

Expand Down Expand Up @@ -247,6 +328,8 @@ export function InformationTab({
</div>
</Section>

<MembersSummary workbenchTenantId={workbenchTenantId} participants={participants} />

<Section
title="Skills"
action={{ to: benchLink(SKILLS_PATH_PREFIX, workbenchTenantId), label: "View all" }}
Expand Down
Loading
Loading