2de893f624
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies. > - The board UI is the main operator surface, so its component and workflow coverage needs to stay reviewable as the product grows. > - This branch adds Storybook as a dedicated UI reference surface for core Paperclip screens and interaction patterns. > - That work spans Storybook infrastructure, app-level provider wiring, and a large fixture set that can render real control-plane states without a live backend. > - The branch also expands coverage across agents, budgets, issues, chat, dialogs, navigation, projects, and data visualization so future UI changes have a concrete visual baseline. > - This pull request packages that Storybook work on top of the latest `master`, excludes the lockfile from the final diff per repo policy, and fixes one fixture contract drift caught during verification. > - The benefit is a single reviewable PR that adds broad UI documentation and regression-surfacing coverage without losing the existing branch work. ## What Changed - Added Storybook 10 wiring for the UI package, including root scripts, UI package scripts, Storybook config, preview wrappers, Tailwind entrypoints, and setup docs. - Added a large fixture-backed data source for Storybook so complex board states can render without a live server. - Added story suites covering foundations, status language, control-plane surfaces, overview, UX labs, agent management, budget and finance, forms and editors, issue management, navigation and layout, chat and comments, data visualization, dialogs and modals, and projects/goals/workspaces. - Adjusted several UI components for Storybook parity so dialogs, menus, keyboard shortcuts, budget markers, markdown editing, and related surfaces render correctly in isolation. - Rebasing work for PR assembly: replayed the branch onto current `master`, removed `pnpm-lock.yaml` from the final PR diff, and aligned the dashboard fixture with the current `DashboardSummary.runActivity` API contract. ## Verification - `pnpm --filter @paperclipai/ui typecheck` - `pnpm --filter @paperclipai/ui build-storybook` - Manual diff audit after rebase: verified the PR no longer includes `pnpm-lock.yaml` and now cleanly targets current `master`. - Before/after UI note: before this branch there was no dedicated Storybook surface for these Paperclip views; after this branch the local Storybook build includes the new overview and domain story suites in `ui/storybook-static`. ## Risks - Large static fixture files can drift from shared types as dashboard and UI contracts evolve; this PR already needed one fixture correction for `runActivity`. - Storybook bundle output includes some large chunks, so future growth may need chunking work if build performance becomes an issue. - Several component tweaks were made for isolated rendering parity, so reviewers should spot-check key board surfaces against the live app behavior. ## Model Used - OpenAI Codex, GPT-5-based coding agent in the Paperclip harness; exact serving model ID is not exposed in-runtime to the agent. - Tool-assisted workflow with terminal execution, git operations, local typecheck/build verification, and GitHub CLI PR creation. - Context window/reasoning mode not surfaced by the harness. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
758 lines
26 KiB
TypeScript
758 lines
26 KiB
TypeScript
import { useState, type ReactNode } from "react";
|
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { Edit3, RotateCcw, Settings2 } from "lucide-react";
|
|
import {
|
|
AGENT_ICON_NAMES,
|
|
type Agent,
|
|
type AgentRuntimeState,
|
|
type CompanySecret,
|
|
type EnvBinding,
|
|
} from "@paperclipai/shared";
|
|
import { ActiveAgentsPanel } from "@/components/ActiveAgentsPanel";
|
|
import { AgentConfigForm, type CreateConfigValues } from "@/components/AgentConfigForm";
|
|
import { defaultCreateValues } from "@/components/agent-config-defaults";
|
|
import {
|
|
DraftInput,
|
|
DraftTextarea,
|
|
Field,
|
|
ToggleField,
|
|
help,
|
|
} from "@/components/agent-config-primitives";
|
|
import { AgentIcon, AgentIconPicker } from "@/components/AgentIconPicker";
|
|
import { AgentProperties } from "@/components/AgentProperties";
|
|
import { RunButton, PauseResumeButton } from "@/components/AgentActionButtons";
|
|
import type { LiveRunForIssue } from "@/api/heartbeats";
|
|
import type { AdapterInfo } from "@/api/adapters";
|
|
import { queryKeys } from "@/lib/queryKeys";
|
|
import { cn } from "@/lib/utils";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { storybookAgents, storybookIssues } from "../fixtures/paperclipData";
|
|
|
|
const COMPANY_ID = "company-storybook";
|
|
const now = new Date("2026-04-20T12:00:00.000Z");
|
|
const recent = (minutesAgo: number) => new Date(now.getTime() - minutesAgo * 60_000);
|
|
|
|
function Section({
|
|
eyebrow,
|
|
title,
|
|
children,
|
|
}: {
|
|
eyebrow: string;
|
|
title: string;
|
|
children: ReactNode;
|
|
}) {
|
|
return (
|
|
<section className="paperclip-story__frame overflow-hidden">
|
|
<div className="border-b border-border px-5 py-4">
|
|
<div className="paperclip-story__label">{eyebrow}</div>
|
|
<h2 className="mt-1 text-xl font-semibold">{title}</h2>
|
|
</div>
|
|
<div className="p-5">{children}</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function agentWith(overrides: Partial<Agent>): Agent {
|
|
return {
|
|
...storybookAgents[0]!,
|
|
...overrides,
|
|
adapterConfig: {
|
|
...storybookAgents[0]!.adapterConfig,
|
|
...(overrides.adapterConfig ?? {}),
|
|
},
|
|
runtimeConfig: {
|
|
...storybookAgents[0]!.runtimeConfig,
|
|
...(overrides.runtimeConfig ?? {}),
|
|
},
|
|
permissions: {
|
|
...storybookAgents[0]!.permissions,
|
|
...(overrides.permissions ?? {}),
|
|
},
|
|
metadata: overrides.metadata ?? storybookAgents[0]!.metadata,
|
|
};
|
|
}
|
|
|
|
const agentManagementAgents: Agent[] = [
|
|
agentWith({
|
|
id: "agent-codex",
|
|
name: "CodexCoder",
|
|
urlKey: "codexcoder",
|
|
status: "running",
|
|
icon: "code",
|
|
role: "engineer",
|
|
title: "Senior Product Engineer",
|
|
reportsTo: "agent-cto",
|
|
capabilities: "Owns full-stack product changes, Storybook coverage, and local verification loops.",
|
|
adapterType: "codex_local",
|
|
adapterConfig: {
|
|
command: "codex",
|
|
model: "gpt-5.4",
|
|
modelReasoningEffort: "high",
|
|
search: true,
|
|
dangerouslyBypassApprovalsAndSandbox: true,
|
|
promptTemplate:
|
|
"You are {{ agent.name }}. Work only on the checked-out issue, keep comments concise, and verify before handoff.",
|
|
instructionsFilePath: "agents/codexcoder/AGENTS.md",
|
|
extraArgs: ["--full-auto"],
|
|
env: {
|
|
OPENAI_API_KEY: { type: "secret_ref", secretId: "secret-openai", version: "latest" },
|
|
PAPERCLIP_TRACE: { type: "plain", value: "storybook" },
|
|
} satisfies Record<string, EnvBinding>,
|
|
timeoutSec: 7200,
|
|
graceSec: 20,
|
|
},
|
|
runtimeConfig: {
|
|
heartbeat: {
|
|
enabled: true,
|
|
intervalSec: 900,
|
|
wakeOnDemand: true,
|
|
cooldownSec: 30,
|
|
maxConcurrentRuns: 2,
|
|
},
|
|
},
|
|
lastHeartbeatAt: recent(2),
|
|
updatedAt: recent(2),
|
|
}),
|
|
agentWith({
|
|
id: "agent-qa",
|
|
name: "QAChecker",
|
|
urlKey: "qachecker",
|
|
status: "idle",
|
|
icon: "shield",
|
|
role: "qa",
|
|
title: "QA Engineer",
|
|
reportsTo: "agent-cto",
|
|
capabilities: "Runs targeted browser checks, release smoke tests, and visual Storybook reviews.",
|
|
adapterType: "claude_local",
|
|
adapterConfig: {
|
|
command: "claude",
|
|
model: "claude-sonnet-4.5",
|
|
effort: "medium",
|
|
dangerouslySkipPermissions: false,
|
|
chrome: true,
|
|
instructionsFilePath: "agents/qachecker/AGENTS.md",
|
|
env: {
|
|
PLAYWRIGHT_HEADLESS: { type: "plain", value: "false" },
|
|
} satisfies Record<string, EnvBinding>,
|
|
},
|
|
runtimeConfig: {
|
|
heartbeat: {
|
|
enabled: false,
|
|
intervalSec: 1800,
|
|
wakeOnDemand: true,
|
|
cooldownSec: 60,
|
|
maxConcurrentRuns: 1,
|
|
},
|
|
},
|
|
lastHeartbeatAt: recent(31),
|
|
updatedAt: recent(31),
|
|
}),
|
|
agentWith({
|
|
id: "agent-cto",
|
|
name: "CTO",
|
|
urlKey: "cto",
|
|
status: "paused",
|
|
icon: "crown",
|
|
role: "cto",
|
|
title: "CTO",
|
|
reportsTo: null,
|
|
capabilities: "Reviews engineering strategy, architecture risk, and high-impact implementation tradeoffs.",
|
|
adapterType: "codex_local",
|
|
pauseReason: "manual",
|
|
pausedAt: recent(18),
|
|
permissions: { canCreateAgents: true },
|
|
adapterConfig: {
|
|
command: "codex",
|
|
model: "gpt-5.4",
|
|
modelReasoningEffort: "xhigh",
|
|
search: false,
|
|
},
|
|
runtimeConfig: {
|
|
heartbeat: {
|
|
enabled: true,
|
|
intervalSec: 3600,
|
|
wakeOnDemand: false,
|
|
cooldownSec: 120,
|
|
maxConcurrentRuns: 1,
|
|
},
|
|
},
|
|
lastHeartbeatAt: recent(57),
|
|
updatedAt: recent(18),
|
|
}),
|
|
agentWith({
|
|
id: "agent-observability",
|
|
name: "OpsWatch",
|
|
urlKey: "opswatch",
|
|
status: "error",
|
|
icon: "radar",
|
|
role: "devops",
|
|
title: "Runtime Operations Engineer",
|
|
reportsTo: "agent-cto",
|
|
capabilities: "Monitors local runners, workspace services, and stuck-run recovery signals.",
|
|
adapterType: "http",
|
|
pauseReason: null,
|
|
pausedAt: null,
|
|
adapterConfig: {
|
|
webhookUrl: "https://ops.internal.example/heartbeat",
|
|
payloadTemplateJson: JSON.stringify({ channel: "paperclip-storybook", priority: "normal" }, null, 2),
|
|
env: {
|
|
OPS_WEBHOOK_TOKEN: { type: "secret_ref", secretId: "secret-ops-webhook", version: 3 },
|
|
} satisfies Record<string, EnvBinding>,
|
|
},
|
|
runtimeConfig: {
|
|
heartbeat: {
|
|
enabled: true,
|
|
intervalSec: 600,
|
|
wakeOnDemand: true,
|
|
cooldownSec: 45,
|
|
maxConcurrentRuns: 1,
|
|
},
|
|
},
|
|
lastHeartbeatAt: recent(9),
|
|
updatedAt: recent(9),
|
|
}),
|
|
];
|
|
|
|
const runtimeState: AgentRuntimeState = {
|
|
agentId: "agent-codex",
|
|
companyId: COMPANY_ID,
|
|
adapterType: "codex_local",
|
|
sessionId: "session-codex-storybook-management-20260420",
|
|
sessionDisplayId: "codex-storybook-20260420",
|
|
sessionParamsJson: {
|
|
issueIdentifier: "PAP-1670",
|
|
workspaceStrategy: "git_worktree",
|
|
},
|
|
stateJson: {
|
|
currentIssue: "PAP-1670",
|
|
workspace: "PAP-1641-create-super-detailed-storybooks-for-our-project",
|
|
},
|
|
lastRunId: "run-agent-management-live",
|
|
lastRunStatus: "running",
|
|
totalInputTokens: 286_400,
|
|
totalOutputTokens: 42_900,
|
|
totalCachedInputTokens: 113_200,
|
|
totalCostCents: 4320,
|
|
lastError: "Previous run lost its Storybook Vite websocket after a local server restart.",
|
|
createdAt: recent(8_000),
|
|
updatedAt: recent(2),
|
|
};
|
|
|
|
const storybookSecrets: CompanySecret[] = [
|
|
{
|
|
id: "secret-openai",
|
|
companyId: COMPANY_ID,
|
|
name: "OPENAI_API_KEY",
|
|
provider: "local_encrypted",
|
|
externalRef: null,
|
|
latestVersion: 5,
|
|
description: "Primary coding model key for local Codex agents.",
|
|
createdByAgentId: null,
|
|
createdByUserId: "user-board",
|
|
createdAt: recent(21_000),
|
|
updatedAt: recent(400),
|
|
},
|
|
{
|
|
id: "secret-ops-webhook",
|
|
companyId: COMPANY_ID,
|
|
name: "OPS_WEBHOOK_TOKEN",
|
|
provider: "local_encrypted",
|
|
externalRef: null,
|
|
latestVersion: 3,
|
|
description: "Webhook token for runtime observability callbacks.",
|
|
createdByAgentId: "agent-cto",
|
|
createdByUserId: null,
|
|
createdAt: recent(12_000),
|
|
updatedAt: recent(80),
|
|
},
|
|
];
|
|
|
|
const adapterFixtures: AdapterInfo[] = [
|
|
{
|
|
type: "codex_local",
|
|
label: "Codex Local",
|
|
source: "builtin",
|
|
modelsCount: 3,
|
|
loaded: true,
|
|
disabled: false,
|
|
capabilities: {
|
|
supportsInstructionsBundle: true,
|
|
supportsSkills: true,
|
|
supportsLocalAgentJwt: true,
|
|
requiresMaterializedRuntimeSkills: true,
|
|
},
|
|
},
|
|
{
|
|
type: "claude_local",
|
|
label: "Claude Local",
|
|
source: "builtin",
|
|
modelsCount: 2,
|
|
loaded: true,
|
|
disabled: false,
|
|
capabilities: {
|
|
supportsInstructionsBundle: true,
|
|
supportsSkills: true,
|
|
supportsLocalAgentJwt: true,
|
|
requiresMaterializedRuntimeSkills: true,
|
|
},
|
|
},
|
|
{
|
|
type: "http",
|
|
label: "HTTP Webhook",
|
|
source: "builtin",
|
|
modelsCount: 0,
|
|
loaded: true,
|
|
disabled: false,
|
|
capabilities: {
|
|
supportsInstructionsBundle: false,
|
|
supportsSkills: false,
|
|
supportsLocalAgentJwt: false,
|
|
requiresMaterializedRuntimeSkills: false,
|
|
},
|
|
},
|
|
];
|
|
|
|
const liveRuns: LiveRunForIssue[] = [
|
|
{
|
|
id: "run-agent-management-live",
|
|
status: "running",
|
|
invocationSource: "assignment",
|
|
triggerDetail: "issue_assigned",
|
|
startedAt: recent(8).toISOString(),
|
|
finishedAt: null,
|
|
createdAt: recent(8).toISOString(),
|
|
agentId: "agent-codex",
|
|
agentName: "CodexCoder",
|
|
adapterType: "codex_local",
|
|
issueId: "issue-storybook-1",
|
|
livenessState: "advanced",
|
|
livenessReason: null,
|
|
continuationAttempt: 0,
|
|
lastUsefulActionAt: recent(1).toISOString(),
|
|
nextAction: "Run a targeted Storybook static build.",
|
|
},
|
|
{
|
|
id: "run-agent-management-queued",
|
|
status: "queued",
|
|
invocationSource: "on_demand",
|
|
triggerDetail: "manual",
|
|
startedAt: null,
|
|
finishedAt: null,
|
|
createdAt: recent(3).toISOString(),
|
|
agentId: "agent-qa",
|
|
agentName: "QAChecker",
|
|
adapterType: "claude_local",
|
|
issueId: "issue-storybook-3",
|
|
livenessState: null,
|
|
livenessReason: "Waiting for current visual review to finish.",
|
|
continuationAttempt: 0,
|
|
lastUsefulActionAt: null,
|
|
nextAction: "Open the Storybook preview and capture mobile screenshots.",
|
|
},
|
|
{
|
|
id: "run-agent-management-succeeded",
|
|
status: "succeeded",
|
|
invocationSource: "timer",
|
|
triggerDetail: "scheduler",
|
|
startedAt: recent(48).toISOString(),
|
|
finishedAt: recent(39).toISOString(),
|
|
createdAt: recent(48).toISOString(),
|
|
agentId: "agent-cto",
|
|
agentName: "CTO",
|
|
adapterType: "codex_local",
|
|
issueId: "issue-storybook-2",
|
|
livenessState: "completed",
|
|
livenessReason: null,
|
|
continuationAttempt: 0,
|
|
lastUsefulActionAt: recent(39).toISOString(),
|
|
nextAction: null,
|
|
},
|
|
{
|
|
id: "run-agent-management-failed",
|
|
status: "failed",
|
|
invocationSource: "automation",
|
|
triggerDetail: "routine",
|
|
startedAt: recent(76).toISOString(),
|
|
finishedAt: recent(70).toISOString(),
|
|
createdAt: recent(76).toISOString(),
|
|
agentId: "agent-observability",
|
|
agentName: "OpsWatch",
|
|
adapterType: "http",
|
|
issueId: null,
|
|
livenessState: "blocked",
|
|
livenessReason: "Webhook returned 503 during local runtime restart.",
|
|
continuationAttempt: 1,
|
|
lastUsefulActionAt: recent(72).toISOString(),
|
|
nextAction: "Retry after runtime service health check recovers.",
|
|
},
|
|
];
|
|
|
|
function StorybookQueryFixtures({ children }: { children: ReactNode }) {
|
|
const queryClient = useQueryClient();
|
|
|
|
queryClient.setQueryData(queryKeys.agents.list(COMPANY_ID), agentManagementAgents);
|
|
queryClient.setQueryData(queryKeys.secrets.list(COMPANY_ID), storybookSecrets);
|
|
queryClient.setQueryData(queryKeys.adapters.all, adapterFixtures);
|
|
queryClient.setQueryData(queryKeys.issues.list(COMPANY_ID), storybookIssues);
|
|
queryClient.setQueryData([...queryKeys.issues.list(COMPANY_ID), "with-routine-executions"], storybookIssues);
|
|
queryClient.setQueryData([...queryKeys.liveRuns(COMPANY_ID), "dashboard"], liveRuns);
|
|
queryClient.setQueryData(queryKeys.instance.generalSettings, { censorUsernameInLogs: false });
|
|
queryClient.setQueryData(queryKeys.agents.adapterModels(COMPANY_ID, "codex_local"), [
|
|
{ id: "gpt-5.4", label: "GPT-5.4" },
|
|
{ id: "gpt-5.4-mini", label: "GPT-5.4 Mini" },
|
|
{ id: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
|
|
]);
|
|
queryClient.setQueryData(queryKeys.agents.detectModel(COMPANY_ID, "codex_local"), {
|
|
model: "gpt-5.4",
|
|
provider: "openai",
|
|
source: "config",
|
|
candidates: ["gpt-5.4", "gpt-5.4-mini"],
|
|
});
|
|
queryClient.setQueryData(queryKeys.agents.adapterModels(COMPANY_ID, "claude_local"), [
|
|
{ id: "claude-sonnet-4.5", label: "Claude Sonnet 4.5" },
|
|
{ id: "claude-opus-4.1", label: "Claude Opus 4.1" },
|
|
]);
|
|
|
|
return children;
|
|
}
|
|
|
|
function AgentConfigFormStory() {
|
|
const [values, setValues] = useState<CreateConfigValues>({
|
|
...defaultCreateValues,
|
|
adapterType: "codex_local",
|
|
command: "codex",
|
|
model: "gpt-5.4",
|
|
thinkingEffort: "high",
|
|
search: true,
|
|
dangerouslyBypassSandbox: true,
|
|
promptTemplate:
|
|
"You are {{ agent.name }}. Read the assigned issue, make a small verified change, and update the task.",
|
|
extraArgs: "--full-auto, --search",
|
|
envBindings: {
|
|
OPENAI_API_KEY: { type: "secret_ref", secretId: "secret-openai", version: "latest" },
|
|
PAPERCLIP_TRACE: { type: "plain", value: "storybook" },
|
|
},
|
|
runtimeServicesJson: JSON.stringify(
|
|
[
|
|
{
|
|
name: "storybook",
|
|
command: "pnpm storybook",
|
|
url: "http://localhost:6006",
|
|
},
|
|
],
|
|
null,
|
|
2,
|
|
),
|
|
heartbeatEnabled: true,
|
|
intervalSec: 900,
|
|
});
|
|
|
|
return (
|
|
<AgentConfigForm
|
|
mode="create"
|
|
values={values}
|
|
onChange={(patch) => setValues((current) => ({ ...current, ...patch }))}
|
|
sectionLayout="cards"
|
|
showAdapterTestEnvironmentButton={false}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function IconPickerMatrix() {
|
|
const [selectedIcon, setSelectedIcon] = useState("code");
|
|
const visibleIcons = AGENT_ICON_NAMES.slice(0, 28);
|
|
|
|
return (
|
|
<div className="grid gap-5 lg:grid-cols-[280px_minmax(0,1fr)]">
|
|
<Card className="shadow-none">
|
|
<CardHeader>
|
|
<CardTitle>Selected identity</CardTitle>
|
|
<CardDescription>The real picker trigger updates the selected fixture state.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex items-center gap-3 rounded-lg border border-border bg-background/70 p-4">
|
|
<div className="flex h-11 w-11 items-center justify-center rounded-lg border border-border bg-accent/40">
|
|
<AgentIcon icon={selectedIcon} className="h-5 w-5" />
|
|
</div>
|
|
<div>
|
|
<div className="text-sm font-medium">StorybookEngineer</div>
|
|
<div className="font-mono text-xs text-muted-foreground">{selectedIcon}</div>
|
|
</div>
|
|
</div>
|
|
<AgentIconPicker value={selectedIcon} onChange={setSelectedIcon}>
|
|
<Button variant="outline" className="w-full justify-start">
|
|
<Settings2 className="h-4 w-4" />
|
|
Open icon picker
|
|
</Button>
|
|
</AgentIconPicker>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className="rounded-xl border border-border bg-background/70 p-4">
|
|
<div className="grid grid-cols-7 gap-2 sm:grid-cols-10 md:grid-cols-14">
|
|
{visibleIcons.map((name) => (
|
|
<button
|
|
key={name}
|
|
type="button"
|
|
title={name}
|
|
onClick={() => setSelectedIcon(name)}
|
|
className={cn(
|
|
"flex h-10 w-10 items-center justify-center rounded-lg border border-border transition-colors hover:bg-accent",
|
|
selectedIcon === name && "border-primary bg-primary/10 text-primary ring-1 ring-primary",
|
|
)}
|
|
>
|
|
<AgentIcon icon={name} className="h-4 w-4" />
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function AgentActionsMatrix() {
|
|
const actionAgents = [
|
|
agentManagementAgents[0]!,
|
|
agentManagementAgents[1]!,
|
|
agentManagementAgents[2]!,
|
|
agentManagementAgents[3]!,
|
|
];
|
|
|
|
return (
|
|
<div className="grid gap-4 xl:grid-cols-4">
|
|
{actionAgents.map((agent) => {
|
|
const paused = agent.status === "paused";
|
|
const runDisabled = agent.status === "running" || agent.status === "paused";
|
|
const restartDisabled = agent.status === "paused";
|
|
|
|
return (
|
|
<Card key={agent.id} className="shadow-none">
|
|
<CardHeader>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex items-center gap-3">
|
|
<span className="flex h-9 w-9 items-center justify-center rounded-lg border border-border bg-accent/40">
|
|
<AgentIcon icon={agent.icon} className="h-4 w-4" />
|
|
</span>
|
|
<div>
|
|
<CardTitle className="text-base">{agent.name}</CardTitle>
|
|
<CardDescription>{agent.title}</CardDescription>
|
|
</div>
|
|
</div>
|
|
<Badge variant={agent.status === "error" ? "destructive" : "outline"}>{agent.status}</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="flex flex-wrap gap-2">
|
|
<PauseResumeButton
|
|
isPaused={paused}
|
|
onPause={() => undefined}
|
|
onResume={() => undefined}
|
|
disabled={agent.status === "running"}
|
|
/>
|
|
<RunButton
|
|
label={agent.status === "running" ? "Running" : "Run now"}
|
|
onClick={() => undefined}
|
|
disabled={runDisabled}
|
|
/>
|
|
<Button variant="outline" size="sm" disabled={restartDisabled}>
|
|
<RotateCcw className="h-3.5 w-3.5 sm:mr-1" />
|
|
<span className="hidden sm:inline">Restart</span>
|
|
</Button>
|
|
<Button variant="ghost" size="sm">
|
|
<Edit3 className="h-3.5 w-3.5 sm:mr-1" />
|
|
<span className="hidden sm:inline">Edit</span>
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ConfigPrimitivesStory() {
|
|
const [textValue, setTextValue] = useState("gpt-5.4");
|
|
const [selectValue, setSelectValue] = useState("git_worktree");
|
|
const [toggleValue, setToggleValue] = useState(true);
|
|
const [jsonValue, setJsonValue] = useState(JSON.stringify({
|
|
runtimeServices: [
|
|
{ name: "api", command: "pnpm dev:once", healthUrl: "http://localhost:3100/api/health" },
|
|
],
|
|
env: { PAPERCLIP_BIND: "lan" },
|
|
}, null, 2));
|
|
|
|
return (
|
|
<div className="grid gap-5 lg:grid-cols-2">
|
|
<div className="space-y-4 rounded-xl border border-border bg-background/70 p-4">
|
|
<Field label="Text field" hint={help.model}>
|
|
<DraftInput
|
|
value={textValue}
|
|
onCommit={setTextValue}
|
|
immediate
|
|
className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 font-mono text-sm outline-none"
|
|
/>
|
|
</Field>
|
|
<Field label="Select field" hint={help.workspaceStrategy}>
|
|
<Select value={selectValue} onValueChange={setSelectValue}>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder="Workspace strategy" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="project_primary">Project primary</SelectItem>
|
|
<SelectItem value="git_worktree">Git worktree</SelectItem>
|
|
<SelectItem value="agent_home">Agent home</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</Field>
|
|
<ToggleField
|
|
label="Toggle field"
|
|
hint={help.wakeOnDemand}
|
|
checked={toggleValue}
|
|
onChange={setToggleValue}
|
|
/>
|
|
</div>
|
|
<div className="rounded-xl border border-border bg-background/70 p-4">
|
|
<Field label="JSON editor" hint={help.runtimeServicesJson}>
|
|
<DraftTextarea
|
|
value={jsonValue}
|
|
onCommit={setJsonValue}
|
|
immediate
|
|
minRows={10}
|
|
placeholder='{"runtimeServices":[]}'
|
|
/>
|
|
</Field>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function AgentManagementStories() {
|
|
return (
|
|
<StorybookQueryFixtures>
|
|
<div className="paperclip-story">
|
|
<main className="paperclip-story__inner space-y-6">
|
|
<section className="paperclip-story__frame p-6">
|
|
<div className="flex flex-wrap items-start justify-between gap-5">
|
|
<div>
|
|
<div className="paperclip-story__label">Agent management</div>
|
|
<h1 className="mt-2 text-3xl font-semibold tracking-tight">Agent details, controls, and config surfaces</h1>
|
|
<p className="mt-3 max-w-3xl text-sm leading-6 text-muted-foreground">
|
|
Management stories exercise the dense pieces of the agent lifecycle: status detail panels,
|
|
adapter configuration, icon identity, run controls, live-agent cards, and the config-field primitives
|
|
used inside the form.
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
<Badge variant="outline">adapter config</Badge>
|
|
<Badge variant="outline">runtime policy</Badge>
|
|
<Badge variant="outline">env bindings</Badge>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<Section eyebrow="AgentProperties" title="Full detail panel with runtime and reporting data">
|
|
<div className="grid gap-5 lg:grid-cols-[380px_minmax(0,1fr)]">
|
|
<Card className="shadow-none">
|
|
<CardHeader>
|
|
<div className="flex items-start gap-3">
|
|
<span className="flex h-11 w-11 items-center justify-center rounded-lg border border-border bg-accent/40">
|
|
<AgentIcon icon={agentManagementAgents[0]!.icon} className="h-5 w-5" />
|
|
</span>
|
|
<div>
|
|
<CardTitle>{agentManagementAgents[0]!.name}</CardTitle>
|
|
<CardDescription>{agentManagementAgents[0]!.capabilities}</CardDescription>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<AgentProperties agent={agentManagementAgents[0]!} runtimeState={runtimeState} />
|
|
</CardContent>
|
|
</Card>
|
|
<div className="rounded-xl border border-border bg-background/70 p-5">
|
|
<div className="mb-4 flex flex-wrap gap-2">
|
|
<Badge variant="secondary">session populated</Badge>
|
|
<Badge variant="secondary">last error shown</Badge>
|
|
<Badge variant="secondary">manager lookup seeded</Badge>
|
|
</div>
|
|
<div className="grid gap-3 text-sm md:grid-cols-2">
|
|
<div className="rounded-lg border border-border p-3">
|
|
<div className="text-xs text-muted-foreground">Budget</div>
|
|
<div className="mt-1 font-mono">${(agentManagementAgents[0]!.budgetMonthlyCents / 100).toFixed(0)} / month</div>
|
|
</div>
|
|
<div className="rounded-lg border border-border p-3">
|
|
<div className="text-xs text-muted-foreground">Spent</div>
|
|
<div className="mt-1 font-mono">${(agentManagementAgents[0]!.spentMonthlyCents / 100).toFixed(0)}</div>
|
|
</div>
|
|
<div className="rounded-lg border border-border p-3">
|
|
<div className="text-xs text-muted-foreground">Instructions</div>
|
|
<div className="mt-1 break-all font-mono text-xs">
|
|
{String(agentManagementAgents[0]!.adapterConfig.instructionsFilePath)}
|
|
</div>
|
|
</div>
|
|
<div className="rounded-lg border border-border p-3">
|
|
<div className="text-xs text-muted-foreground">Runtime policy</div>
|
|
<div className="mt-1 font-mono text-xs">heartbeat / 900s / max 2</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Section>
|
|
|
|
<Section eyebrow="AgentConfigForm" title="Adapter selection, runtime config, and env vars">
|
|
<div className="max-w-4xl">
|
|
<AgentConfigFormStory />
|
|
</div>
|
|
</Section>
|
|
|
|
<Section eyebrow="AgentIconPicker" title="Available icon grid with selected state">
|
|
<IconPickerMatrix />
|
|
</Section>
|
|
|
|
<Section eyebrow="AgentActionButtons" title="Pause, resume, restart, edit, and run actions by state">
|
|
<AgentActionsMatrix />
|
|
</Section>
|
|
|
|
<Section eyebrow="ActiveAgentsPanel" title="Mixed live, queued, succeeded, and failed agent runs">
|
|
<ActiveAgentsPanel companyId={COMPANY_ID} />
|
|
</Section>
|
|
|
|
<Section eyebrow="agent-config-primitives" title="Individual text, select, toggle, and JSON field types">
|
|
<ConfigPrimitivesStory />
|
|
</Section>
|
|
|
|
<Separator />
|
|
</main>
|
|
</div>
|
|
</StorybookQueryFixtures>
|
|
);
|
|
}
|
|
|
|
const meta = {
|
|
title: "Product/Agent Management",
|
|
component: AgentManagementStories,
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
component:
|
|
"Agent management stories cover detail, configuration, icon, action, live-run, and config primitive states using extended Paperclip fixtures.",
|
|
},
|
|
},
|
|
},
|
|
} satisfies Meta<typeof AgentManagementStories>;
|
|
|
|
export default meta;
|
|
|
|
type Story = StoryObj<typeof meta>;
|
|
|
|
export const ManagementMatrix: Story = {};
|