From de0f28e0dd8c29a67b13def01a7ef1d809043994 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 30 Sep 2026 14:29:15 -0700 Subject: [PATCH] fix(web): the worker page has the mockup's tabs, status card, danger card and details (CL-9667) --- apps/web/src/bench/approvals-insights.tsx | 4 +- apps/web/src/pages/worker-page.css | 230 ++++++++++++++ apps/web/src/pages/worker-page.tsx | 354 ++++++++++++++-------- apps/web/src/shell/page-layout.css | 7 +- apps/web/src/shell/page-layout.tsx | 10 +- 5 files changed, 469 insertions(+), 136 deletions(-) create mode 100644 apps/web/src/pages/worker-page.css diff --git a/apps/web/src/bench/approvals-insights.tsx b/apps/web/src/bench/approvals-insights.tsx index e020151c3..58f7552a0 100644 --- a/apps/web/src/bench/approvals-insights.tsx +++ b/apps/web/src/bench/approvals-insights.tsx @@ -14,12 +14,12 @@ const ROWS = [ ["waiting", "Waiting on you", "var(--action)"], ] as const; -type ApprovalTally = +export type ApprovalTally = | { readonly kind: "loading" } | { readonly kind: "error" } | { readonly kind: "ready"; readonly counts: ApprovalCounts }; -function useApprovalTally(tenantId: string, runs: readonly InsightsRun[]): ApprovalTally { +export function useApprovalTally(tenantId: string, runs: readonly InsightsRun[]): ApprovalTally { const results = useQueries({ queries: runs.map((run) => apiQueryOptions(insightsRunApprovalsPath(tenantId, run.id), RunApprovalsResponse), diff --git a/apps/web/src/pages/worker-page.css b/apps/web/src/pages/worker-page.css new file mode 100644 index 000000000..db75aba70 --- /dev/null +++ b/apps/web/src/pages/worker-page.css @@ -0,0 +1,230 @@ +/* Worker detail: raised panel cards, tab strip, permissions rows, danger card + (mockup agent.html + components.css). */ +.page-layout-head--hero .page-layout-lede { + margin-top: 4px; + font-size: 14.5px; +} + +.wp-open { + display: inline-flex; + align-items: center; + gap: 8px; + height: 36px; + padding: 0 16px; + border-radius: var(--r-md); + background: var(--primary); + color: var(--primary-foreground); + font-size: 14px; + font-weight: 700; + text-decoration: none; +} +.wp-open svg { + color: var(--action); +} + +.wp-tabs { + display: flex; + gap: 4px; + margin: 24px 0; + border-bottom: 1px solid var(--line); +} +.wp-tabs button { + height: 40px; + padding: 0 4px; + margin-right: 16px; + border: 0; + background: none; + font-size: 14px; + font-weight: 700; + color: var(--ink-3); + box-shadow: inset 0 -2px 0 transparent; + transition: + color 120ms ease, + box-shadow 150ms ease-out; +} +.wp-tabs button:hover { + color: var(--ink); +} +.wp-tabs button.active { + color: var(--ink); + box-shadow: inset 0 -2px 0 var(--ink); +} + +.wp-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) 300px; + gap: 24px; + align-items: start; +} +@media (max-width: 960px) { + .wp-grid { + grid-template-columns: 1fr; + } +} +.wp-stack { + display: flex; + flex-direction: column; + gap: 12px; +} + +.wp-card { + padding: 20px; + border-radius: var(--r-lg); + background: var(--surface); + box-shadow: var(--raised); +} +.wp-card h2 { + margin: 0 0 4px; + font-size: 16px; + font-weight: 800; +} +.wp-card h2.wp-h2--small { + margin-bottom: 12px; + font-size: 14px; +} +.wp-sub { + margin: 0 0 16px; + font-size: 13.5px; + color: var(--ink-3); +} +.wp-empty { + margin: 0; + font-size: 13.5px; + color: var(--ink-3); +} + +.wp-status { + display: flex; + align-items: center; + gap: 8px; + font-size: 13.5px; + color: var(--ink-2); +} + +.wp-kv { + display: grid; + grid-template-columns: 100px 1fr; + gap: 10px 16px; + margin: 0; + font-size: 13.5px; +} +.wp-kv dt { + color: var(--ink-3); +} +.wp-kv dd { + margin: 0; + min-width: 0; + font-weight: 600; +} + +.wp-perm { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + gap: 12px; + padding: 12px 0; + border-top: 1px solid var(--line); +} +.wp-perm:first-child { + border-top: 0; + padding-top: 0; +} +.wp-perm b { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + font-family: var(--font-mono); + font-size: 12.5px; + font-weight: 400; +} +.wp-seg { + display: inline-flex; + gap: 2px; + padding: 2px; + border-radius: var(--r-md); + background: var(--hover); +} +.wp-seg button { + height: 26px; + padding: 0 10px; + border: 0; + border-radius: 6px; + background: transparent; + font-size: 12.5px; + font-weight: 700; + color: var(--ink-3); +} +.wp-seg button:hover { + color: var(--ink); +} +.wp-seg button.active { + background: var(--surface); + color: var(--ink); + box-shadow: var(--raised); +} + +.wp-li { + display: flex; + align-items: center; + gap: 10px; + min-height: 40px; + padding: 6px 8px; + margin: 0 -8px; + border-radius: var(--r-md); + color: inherit; + text-decoration: none; + transition: background-color 120ms ease; +} +.wp-li:hover { + background: var(--hover); +} +.wp-li svg { + flex: none; + color: var(--ink-3); +} +.wp-li b { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 13.5px; + font-weight: 600; +} + +.wp-runs { + display: grid; +} +.wp-run { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 10px 0; + border-top: 1px solid var(--line); + font-size: 13.5px; +} +.wp-run:first-child { + border-top: 0; + padding-top: 0; +} +.wp-run span { + color: var(--ink-3); +} + +.wp-danger { + display: flex; + flex-direction: column; + gap: 16px; + box-shadow: 0 0 0 1px color-mix(in srgb, var(--danger) 22%, transparent); +} +.wp-danger h2 { + font-size: 15px; +} +.wp-danger .wp-sub { + max-width: 52ch; + margin: 0; +} +.wp-danger-actions { + display: flex; + justify-content: flex-end; +} diff --git a/apps/web/src/pages/worker-page.tsx b/apps/web/src/pages/worker-page.tsx index 82b6fe759..f2487e15e 100644 --- a/apps/web/src/pages/worker-page.tsx +++ b/apps/web/src/pages/worker-page.tsx @@ -2,7 +2,7 @@ // and the workbench it lives on. import { useState } from "react"; -import { Button, Card, CardDescription, CardTitle, PageShell, Skeleton } from "@corbits/react-ui"; +import { Button, Skeleton } from "@corbits/react-ui"; import { toast } from "@corbits/react-ui/ui/toast"; import { reportError } from "@corbits/error-sink"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; @@ -10,9 +10,14 @@ import type { GrantEffect } from "@intx/types"; import { WorkbenchAvatar } from "@/chat/avatar"; import { isDefaultWorker, type ChatAgent } from "@/chat/threads-api"; -import { ChatCircle } from "@/lib/icons"; +import { CaretRight, ChatCircle, Hash } from "@/lib/icons"; import { describeApiError } from "@/lib/api-query"; -import { listTopLevelRuns } from "../agents-api"; +import { ConfirmButton } from "../components/confirm-button"; +import { useAPIQuery } from "../api"; +import { useApprovalTally } from "../bench/approvals-insights"; +import { insightsTopLevelRunsPath, TopLevelRunsSchema } from "../insights-api"; +import { PageLayout } from "../shell/page-layout"; +import { useWorkerRole } from "../worker-role-query"; import { updateGrant, type Grant } from "../settings/tenancy-api"; import { agentSlugFromSourceAssetName, deployAgentSource } from "../agent-deploy"; import { readAgentMcpHandles, readAgentSource } from "../agent-source-read"; @@ -29,7 +34,10 @@ import { StageTopBar } from "../shell/stage-top-bar"; import { WORKERS_PATH_PREFIX } from "../path-ids"; import { workbenchPath } from "../workbench-path"; import { useBenchWorkers } from "../worker-benches"; -import { StatusPill, WorkerRole, workerStatus } from "./workers-page"; +import type { HubTenant } from "../needs-converge"; +import { StatusPill, workerStatus } from "./workers-page"; + +import "./worker-page.css"; function sourceKey(tenantId: string, agentId: string) { return [...tenantKeys.agents(tenantId), "source", agentId] as const; @@ -86,11 +94,11 @@ function InstructionsCard({ }); return ( - - Instructions - +
+

Instructions

+

How this worker should behave. It reads these at the start of every run. - +

{source.isPending ? (

Loading instructions…

) : source.isError ? ( @@ -101,7 +109,7 @@ function InstructionsCard({ <>