[codex] add comprehensive UI Storybook coverage (#4132)
## 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>
2026-04-20 12:13:23 -05:00
|
|
|
import { useMemo, useState, type ReactNode } from "react";
|
|
|
|
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
|
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
|
|
|
import type { Goal, Project } from "@paperclipai/shared";
|
|
|
|
|
import { Archive, Boxes, FolderGit2, GitBranch, Network, Play, RotateCcw, Square } from "lucide-react";
|
|
|
|
|
import { GoalProperties } from "@/components/GoalProperties";
|
|
|
|
|
import { GoalTree } from "@/components/GoalTree";
|
|
|
|
|
import { ProjectProperties, type ProjectConfigFieldKey, type ProjectFieldSaveState } from "@/components/ProjectProperties";
|
|
|
|
|
import { ProjectWorkspacesContent } from "@/components/ProjectWorkspacesContent";
|
|
|
|
|
import { ProjectWorkspaceSummaryCard } from "@/components/ProjectWorkspaceSummaryCard";
|
|
|
|
|
import {
|
|
|
|
|
WorkspaceRuntimeControls,
|
|
|
|
|
buildWorkspaceRuntimeControlSections,
|
|
|
|
|
type WorkspaceRuntimeControlRequest,
|
|
|
|
|
} from "@/components/WorkspaceRuntimeControls";
|
|
|
|
|
import { WorktreeBanner } from "@/components/WorktreeBanner";
|
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
|
|
import { queryKeys } from "@/lib/queryKeys";
|
|
|
|
|
import { buildProjectWorkspaceSummaries } from "@/lib/project-workspaces-tab";
|
|
|
|
|
import {
|
|
|
|
|
storybookAgents,
|
|
|
|
|
storybookAuthSession,
|
|
|
|
|
storybookCompanies,
|
|
|
|
|
storybookExecutionWorkspaces,
|
|
|
|
|
storybookGoals,
|
|
|
|
|
storybookIssues,
|
|
|
|
|
storybookProjectWorkspaces,
|
|
|
|
|
storybookProjects,
|
|
|
|
|
} from "../fixtures/paperclipData";
|
|
|
|
|
|
|
|
|
|
const COMPANY_ID = "company-storybook";
|
|
|
|
|
const boardProject = storybookProjects.find((project) => project.id === "project-board-ui") ?? storybookProjects[0]!;
|
|
|
|
|
const archivedProject =
|
|
|
|
|
storybookProjects.find((project) => project.id === "project-archived-import")
|
|
|
|
|
?? storybookProjects[storybookProjects.length - 1]!;
|
|
|
|
|
|
|
|
|
|
const goalProgress = new Map<string, number>([
|
|
|
|
|
["goal-company", 62],
|
|
|
|
|
["goal-board-ux", 74],
|
|
|
|
|
["goal-agent-runtime", 48],
|
|
|
|
|
["goal-storybook", 88],
|
|
|
|
|
["goal-budget-safety", 100],
|
|
|
|
|
["goal-archived-import", 18],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
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 hydrateStorybookQueries(queryClient: ReturnType<typeof useQueryClient>) {
|
|
|
|
|
queryClient.setQueryData(queryKeys.auth.session, storybookAuthSession);
|
Add cheap model profiles for local adapters (#4881)
## Thinking Path
> - Paperclip is a control plane for autonomous AI companies, where
adapters are the boundary between the board, agents, and execution
runtimes.
> - Local adapters currently expose a primary runtime configuration, but
operators often need a cheaper model lane for routine or low-risk work.
> - That cheap lane has to stay adapter-owned: runtime profile settings
should not mutate the primary adapter config or bypass existing
auth/secret mediation.
> - Issue creation also needs an ergonomic way to request primary,
cheap, or custom model behavior for a selected assignee.
> - This pull request adds a first-class `cheap` model profile contract
across adapter capabilities, heartbeat config resolution, agent
configuration, and issue creation.
> - The benefit is cheaper task execution can be configured and
requested explicitly while preserving adapter boundaries, secret
handling, and audit visibility.
## What Changed
- Added adapter model-profile capability metadata and a `cheap` profile
contract for supported local adapters.
- Applied `runtimeConfig.modelProfiles.cheap.adapterConfig` during
heartbeat config resolution, including requested/applied/fallback run
metadata.
- Added agent configuration UI for cheap model profile settings without
writing those settings into primary `adapterConfig`.
- Added New Issue assignee model lane controls for Primary / Cheap /
Custom and request payload handling.
- Added run ledger profile badges and Storybook stories for the new
cheap-lane UI states.
- Added tests for validators, heartbeat model profile application,
permission/secret mediation, UI payload helpers, and run ledger
rendering.
- Added committed UI verification screenshots under
`docs/pr-screenshots/pap-2837/`.
- Addressed Greptile review feedback around cheap-profile defaults,
shared profile types, and fallback test data.
## Verification
Local:
- `pnpm exec vitest run packages/shared/src/validators/issue.test.ts
server/src/__tests__/adapter-registry.test.ts
server/src/__tests__/agent-permissions-routes.test.ts
server/src/__tests__/heartbeat-model-profile.test.ts
ui/src/components/IssueRunLedger.test.tsx
ui/src/lib/agent-config-patch.test.ts
ui/src/lib/issue-assignee-overrides.test.ts
ui/src/lib/new-agent-runtime-config.test.ts` — passed, 8 files / 103
tests.
- `pnpm exec vitest run ui/src/lib/new-agent-runtime-config.test.ts
ui/src/components/IssueRunLedger.test.tsx` — passed after
Greptile/rebase follow-up, 2 files / 17 tests.
- `pnpm --filter @paperclipai/ui typecheck` — passed after
Greptile/rebase follow-up.
- `pnpm -r typecheck` — passed.
- `pnpm build` — passed.
- `pnpm test:run` — did not complete successfully in this local
worktree: it stopped in pre-existing `@paperclipai/adapter-utils`
sandbox/SSH fixture suites outside this PR diff. Failures were 5s local
timeouts plus `git init -b` unsupported by this machine's Git 2.21.0.
The branch-specific targeted suites above passed.
- Branch was fetched/rebased onto `public-gh/master`; `git rev-list
--left-right --count public-gh/master...HEAD` reports `0 9`.
Remote PR checks on latest head
`e30bf399146451c86cee98ed528d51d33fa5af5a`:
- `policy` — passed.
- `verify` — passed.
- `e2e` — passed.
- `Greptile Review` — passed, confidence score 5/5; Greptile review
threads resolved.
- `security/snyk (cryppadotta)` — passed.
Screenshots:
- [New issue cheap lane
desktop](https://github.com/paperclipai/paperclip/blob/PAP-2837-plan-cheap-model-for-adapters-that-can-support-it/docs/pr-screenshots/pap-2837/newissue-cheap-desktop.png)
- [New issue custom lane
desktop](https://github.com/paperclipai/paperclip/blob/PAP-2837-plan-cheap-model-for-adapters-that-can-support-it/docs/pr-screenshots/pap-2837/newissue-custom-desktop.png)
- [New issue unsupported adapter
desktop](https://github.com/paperclipai/paperclip/blob/PAP-2837-plan-cheap-model-for-adapters-that-can-support-it/docs/pr-screenshots/pap-2837/newissue-unsupported-desktop.png)
- [Run ledger model profile badges
desktop](https://github.com/paperclipai/paperclip/blob/PAP-2837-plan-cheap-model-for-adapters-that-can-support-it/docs/pr-screenshots/pap-2837/runledger-profile-badges-desktop.png)
- Mobile variants are also in `docs/pr-screenshots/pap-2837/`.
## Risks
- Medium: heartbeat config mediation now merges runtime model profiles
into adapter configs, so adapter secret normalization and host-command
restrictions must keep covering nested config paths.
- Medium: the UI adds another issue creation choice; unsupported
adapters must keep hiding the cheap lane and preserve primary behavior.
- Low migration risk: no database migration is included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
OpenAI Codex coding agent using GPT-5-class reasoning with repo tool use
and command execution. Exact served model/context window was not exposed
by the runtime.
## 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
- [ ] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] 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>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 15:32:04 -05:00
|
|
|
queryClient.setQueryData(queryKeys.companies.all, { companies: storybookCompanies, unauthorized: false });
|
[codex] add comprehensive UI Storybook coverage (#4132)
## 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>
2026-04-20 12:13:23 -05:00
|
|
|
queryClient.setQueryData(queryKeys.agents.list(COMPANY_ID), storybookAgents);
|
|
|
|
|
queryClient.setQueryData(queryKeys.projects.list(COMPANY_ID), storybookProjects);
|
|
|
|
|
queryClient.setQueryData(queryKeys.projects.detail(boardProject.id), boardProject);
|
|
|
|
|
queryClient.setQueryData(queryKeys.projects.detail(boardProject.urlKey), boardProject);
|
|
|
|
|
queryClient.setQueryData(queryKeys.projects.detail(archivedProject.id), archivedProject);
|
|
|
|
|
queryClient.setQueryData(queryKeys.goals.list(COMPANY_ID), storybookGoals);
|
|
|
|
|
for (const goal of storybookGoals) {
|
|
|
|
|
queryClient.setQueryData(queryKeys.goals.detail(goal.id), goal);
|
|
|
|
|
}
|
|
|
|
|
queryClient.setQueryData(queryKeys.issues.list(COMPANY_ID), storybookIssues);
|
|
|
|
|
queryClient.setQueryData(queryKeys.issues.listByProject(COMPANY_ID, boardProject.id), storybookIssues);
|
|
|
|
|
queryClient.setQueryData(queryKeys.secrets.list(COMPANY_ID), []);
|
|
|
|
|
queryClient.setQueryData(queryKeys.instance.experimentalSettings, {
|
|
|
|
|
enableIsolatedWorkspaces: true,
|
|
|
|
|
enableRoutineTriggers: true,
|
|
|
|
|
});
|
|
|
|
|
queryClient.setQueryData(queryKeys.executionWorkspaces.list(COMPANY_ID), storybookExecutionWorkspaces);
|
|
|
|
|
queryClient.setQueryData(
|
|
|
|
|
queryKeys.executionWorkspaces.list(COMPANY_ID, { projectId: boardProject.id }),
|
|
|
|
|
storybookExecutionWorkspaces,
|
|
|
|
|
);
|
|
|
|
|
queryClient.setQueryData(
|
|
|
|
|
queryKeys.executionWorkspaces.summaryList(COMPANY_ID),
|
|
|
|
|
storybookExecutionWorkspaces.map((workspace) => ({
|
|
|
|
|
id: workspace.id,
|
|
|
|
|
name: workspace.name,
|
|
|
|
|
mode: workspace.mode,
|
|
|
|
|
projectWorkspaceId: workspace.projectWorkspaceId,
|
|
|
|
|
})),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function StorybookData({ children }: { children: ReactNode }) {
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
const [ready] = useState(() => {
|
|
|
|
|
hydrateStorybookQueries(queryClient);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return ready ? children : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function stateForProjectField(field: ProjectConfigFieldKey): ProjectFieldSaveState {
|
|
|
|
|
if (field === "env" || field === "execution_workspace_branch_template") return "saved";
|
|
|
|
|
if (field === "execution_workspace_worktree_parent_dir") return "saving";
|
|
|
|
|
return "idle";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ProjectPropertiesMatrix() {
|
|
|
|
|
const editableProject: Project = useMemo(
|
|
|
|
|
() => ({
|
|
|
|
|
...boardProject,
|
|
|
|
|
env: {
|
|
|
|
|
STORYBOOK_REVIEW: { type: "plain", value: "enabled" },
|
|
|
|
|
OPENAI_API_KEY: { type: "secret_ref", secretId: "secret-openai", version: "latest" },
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="grid gap-5 xl:grid-cols-[minmax(0,1fr)_420px]">
|
|
|
|
|
<div className="rounded-lg border border-border bg-background p-4">
|
|
|
|
|
<ProjectProperties
|
|
|
|
|
project={editableProject}
|
|
|
|
|
onFieldUpdate={() => undefined}
|
|
|
|
|
getFieldSaveState={stateForProjectField}
|
|
|
|
|
onArchive={() => undefined}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="rounded-lg border border-border bg-background p-4">
|
|
|
|
|
<div className="mb-3 flex items-center justify-between gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-sm font-medium">{archivedProject.name}</div>
|
|
|
|
|
<div className="text-xs text-muted-foreground">Archived, no workspace configured</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Badge variant="outline" className="gap-1">
|
|
|
|
|
<Archive className="h-3 w-3" />
|
|
|
|
|
archived
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
<ProjectProperties
|
|
|
|
|
project={archivedProject}
|
|
|
|
|
onFieldUpdate={() => undefined}
|
|
|
|
|
onArchive={() => undefined}
|
|
|
|
|
archivePending={false}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="grid gap-3 sm:grid-cols-3 xl:grid-cols-1">
|
|
|
|
|
{[
|
|
|
|
|
{ label: "Goals linked", value: boardProject.goalIds.length, icon: Network },
|
|
|
|
|
{ label: "Workspaces", value: boardProject.workspaces.length, icon: Boxes },
|
|
|
|
|
{ label: "Runtime services", value: boardProject.primaryWorkspace?.runtimeServices?.length ?? 0, icon: Play },
|
|
|
|
|
].map((item) => {
|
|
|
|
|
const Icon = item.icon;
|
|
|
|
|
return (
|
|
|
|
|
<div key={item.label} className="rounded-lg border border-border bg-background p-4">
|
|
|
|
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
|
|
|
|
<div className="mt-3 text-2xl font-semibold">{item.value}</div>
|
|
|
|
|
<div className="text-xs text-muted-foreground">{item.label}</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function WorkspacesMatrix() {
|
|
|
|
|
const summaries = buildProjectWorkspaceSummaries({
|
|
|
|
|
project: boardProject,
|
|
|
|
|
issues: storybookIssues.filter((issue) => issue.projectId === boardProject.id),
|
|
|
|
|
executionWorkspaces: storybookExecutionWorkspaces,
|
|
|
|
|
});
|
|
|
|
|
const localSummary = summaries.find((summary) => summary.kind === "project_workspace" && summary.workspaceId === "workspace-board-ui");
|
|
|
|
|
const remoteSummary = summaries.find((summary) => summary.workspaceId === "workspace-docs-remote");
|
|
|
|
|
const cleanupSummary = summaries.find((summary) => summary.executionWorkspaceStatus === "cleanup_failed");
|
|
|
|
|
const featuredSummaries = [localSummary, remoteSummary, cleanupSummary].filter(
|
|
|
|
|
(summary): summary is NonNullable<typeof summary> => Boolean(summary),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-5">
|
|
|
|
|
<ProjectWorkspacesContent
|
|
|
|
|
companyId={COMPANY_ID}
|
|
|
|
|
projectId={boardProject.id}
|
|
|
|
|
projectRef={boardProject.urlKey}
|
|
|
|
|
summaries={summaries}
|
|
|
|
|
/>
|
|
|
|
|
<div className="grid gap-4 xl:grid-cols-3">
|
|
|
|
|
{featuredSummaries.map((summary) => (
|
|
|
|
|
<ProjectWorkspaceSummaryCard
|
|
|
|
|
key={summary.key}
|
|
|
|
|
projectRef={boardProject.urlKey}
|
|
|
|
|
summary={summary}
|
|
|
|
|
runtimeActionKey={summary.runningServiceCount > 0 ? `${summary.key}:stop` : null}
|
|
|
|
|
runtimeActionPending={summary.runningServiceCount > 0}
|
|
|
|
|
onRuntimeAction={() => undefined}
|
|
|
|
|
onCloseWorkspace={() => undefined}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="rounded-lg border border-dashed border-border p-5 text-sm text-muted-foreground">
|
|
|
|
|
<ProjectWorkspacesContent
|
|
|
|
|
companyId={COMPANY_ID}
|
|
|
|
|
projectId={archivedProject.id}
|
|
|
|
|
projectRef={archivedProject.urlKey}
|
|
|
|
|
summaries={[]}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function GoalProgressRow({ goal }: { goal: Goal }) {
|
|
|
|
|
const progress = goalProgress.get(goal.id) ?? 0;
|
|
|
|
|
const childCount = storybookGoals.filter((candidate) => candidate.parentId === goal.id).length;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="rounded-lg border border-border bg-background p-3">
|
|
|
|
|
<div className="flex items-start justify-between gap-3">
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<div className="truncate text-sm font-medium">{goal.title}</div>
|
|
|
|
|
<div className="mt-1 text-xs text-muted-foreground">
|
|
|
|
|
{goal.level} · {childCount} child goal{childCount === 1 ? "" : "s"}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span className="font-mono text-xs text-muted-foreground">{progress}%</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-3 h-2 overflow-hidden rounded-full bg-muted" aria-label={`${progress}% complete`}>
|
|
|
|
|
<div className="h-full rounded-full bg-primary" style={{ width: `${progress}%` }} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function GoalPropertiesMatrix() {
|
|
|
|
|
const selectedGoal = storybookGoals.find((goal) => goal.id === "goal-board-ux") ?? storybookGoals[0]!;
|
|
|
|
|
const childGoals = storybookGoals.filter((goal) => goal.parentId === selectedGoal.id);
|
|
|
|
|
const linkedProjects = storybookProjects.filter((project) => project.goalIds.includes(selectedGoal.id));
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_340px]">
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="rounded-lg border border-border bg-background p-5">
|
|
|
|
|
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="paperclip-story__label">Goal detail composition</div>
|
|
|
|
|
<h3 className="mt-2 text-xl font-semibold">{selectedGoal.title}</h3>
|
|
|
|
|
<p className="mt-2 max-w-2xl text-sm leading-6 text-muted-foreground">{selectedGoal.description}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Badge variant="outline">{selectedGoal.status}</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-5 grid gap-3 md:grid-cols-3">
|
|
|
|
|
<GoalProgressRow goal={selectedGoal} />
|
|
|
|
|
<div className="rounded-lg border border-border bg-background p-3">
|
|
|
|
|
<div className="text-2xl font-semibold">{childGoals.length}</div>
|
|
|
|
|
<div className="text-xs text-muted-foreground">Child goals</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="rounded-lg border border-border bg-background p-3">
|
|
|
|
|
<div className="text-2xl font-semibold">{linkedProjects.length}</div>
|
|
|
|
|
<div className="text-xs text-muted-foreground">Linked projects</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="grid gap-3 md:grid-cols-2">
|
|
|
|
|
{childGoals.map((goal) => (
|
|
|
|
|
<GoalProgressRow key={goal.id} goal={goal} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="rounded-lg border border-border bg-background p-4">
|
|
|
|
|
<GoalProperties goal={selectedGoal} onUpdate={() => undefined} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function GoalTreeMatrix() {
|
|
|
|
|
const [selectedGoal, setSelectedGoal] = useState<Goal | null>(storybookGoals[1] ?? null);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_320px]">
|
|
|
|
|
<div className="overflow-hidden rounded-lg border border-border bg-background">
|
|
|
|
|
<GoalTree
|
|
|
|
|
goals={storybookGoals}
|
|
|
|
|
onSelect={setSelectedGoal}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="rounded-lg border border-border bg-background p-4">
|
|
|
|
|
<div className="paperclip-story__label">Selected goal</div>
|
|
|
|
|
{selectedGoal ? (
|
|
|
|
|
<div className="mt-3 space-y-3">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-sm font-medium">{selectedGoal.title}</div>
|
|
|
|
|
<div className="mt-1 text-xs text-muted-foreground">{selectedGoal.description}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<GoalProgressRow goal={selectedGoal} />
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="mt-3 text-sm text-muted-foreground">Select a goal row to inspect its progress state.</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function RuntimeControlsMatrix() {
|
|
|
|
|
const primaryWorkspace = storybookProjectWorkspaces[0]!;
|
|
|
|
|
const remoteWorkspace = storybookProjectWorkspaces.find((workspace) => workspace.id === "workspace-docs-remote")!;
|
|
|
|
|
const runningSections = buildWorkspaceRuntimeControlSections({
|
|
|
|
|
runtimeConfig: primaryWorkspace.runtimeConfig?.workspaceRuntime,
|
|
|
|
|
runtimeServices: primaryWorkspace.runtimeServices,
|
|
|
|
|
canStartServices: true,
|
|
|
|
|
canRunJobs: true,
|
|
|
|
|
});
|
|
|
|
|
const stoppedSections = buildWorkspaceRuntimeControlSections({
|
|
|
|
|
runtimeConfig: remoteWorkspace.runtimeConfig?.workspaceRuntime,
|
|
|
|
|
runtimeServices: remoteWorkspace.runtimeServices,
|
|
|
|
|
canStartServices: true,
|
|
|
|
|
canRunJobs: true,
|
|
|
|
|
});
|
|
|
|
|
const disabledSections = buildWorkspaceRuntimeControlSections({
|
|
|
|
|
runtimeConfig: {
|
|
|
|
|
commands: [
|
|
|
|
|
{ id: "web", name: "Web app", kind: "service", command: "pnpm dev" },
|
|
|
|
|
{ id: "migrate", name: "Migrate database", kind: "job", command: "pnpm db:migrate" },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
runtimeServices: [],
|
|
|
|
|
canStartServices: false,
|
|
|
|
|
canRunJobs: false,
|
|
|
|
|
});
|
|
|
|
|
const pendingRequest: WorkspaceRuntimeControlRequest = {
|
|
|
|
|
action: "restart",
|
|
|
|
|
workspaceCommandId: "storybook",
|
|
|
|
|
runtimeServiceId: "service-storybook",
|
|
|
|
|
serviceIndex: 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="grid gap-5 xl:grid-cols-3">
|
|
|
|
|
<Card className="shadow-none">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="flex items-center gap-2">
|
|
|
|
|
<Square className="h-4 w-4" />
|
|
|
|
|
Running services
|
|
|
|
|
</CardTitle>
|
|
|
|
|
<CardDescription>Stop and restart actions with a pending request spinner.</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<WorkspaceRuntimeControls
|
|
|
|
|
sections={runningSections}
|
|
|
|
|
isPending
|
|
|
|
|
pendingRequest={pendingRequest}
|
|
|
|
|
onAction={() => undefined}
|
|
|
|
|
/>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
<Card className="shadow-none">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="flex items-center gap-2">
|
|
|
|
|
<Play className="h-4 w-4" />
|
|
|
|
|
Stopped remote preview
|
|
|
|
|
</CardTitle>
|
|
|
|
|
<CardDescription>Startable remote workspace service with URL history.</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<WorkspaceRuntimeControls sections={stoppedSections} onAction={() => undefined} />
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
<Card className="shadow-none">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="flex items-center gap-2">
|
|
|
|
|
<RotateCcw className="h-4 w-4" />
|
|
|
|
|
Missing prerequisites
|
|
|
|
|
</CardTitle>
|
|
|
|
|
<CardDescription>Disabled runtime controls when no workspace path is available.</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<WorkspaceRuntimeControls
|
|
|
|
|
sections={disabledSections}
|
|
|
|
|
disabledHint="Add a workspace path before starting runtime services."
|
|
|
|
|
square
|
|
|
|
|
onAction={() => undefined}
|
|
|
|
|
/>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setWorktreeMeta(name: string, content: string) {
|
|
|
|
|
if (typeof document === "undefined") return;
|
|
|
|
|
let element = document.querySelector(`meta[name="${name}"]`);
|
|
|
|
|
if (!element) {
|
|
|
|
|
element = document.createElement("meta");
|
|
|
|
|
element.setAttribute("name", name);
|
|
|
|
|
document.head.appendChild(element);
|
|
|
|
|
}
|
|
|
|
|
element.setAttribute("content", content);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function WorktreeBannerMatrix() {
|
|
|
|
|
setWorktreeMeta("paperclip-worktree-enabled", "true");
|
|
|
|
|
setWorktreeMeta("paperclip-worktree-name", "PAP-1675-projects-goals-workspaces");
|
|
|
|
|
setWorktreeMeta("paperclip-worktree-color", "#0f766e");
|
|
|
|
|
setWorktreeMeta("paperclip-worktree-text-color", "#ecfeff");
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="overflow-hidden rounded-lg border border-border bg-background">
|
|
|
|
|
<WorktreeBanner />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="grid gap-3 md:grid-cols-3">
|
|
|
|
|
{[
|
|
|
|
|
{ label: "Branch", value: "PAP-1675-projects-goals-workspaces", icon: GitBranch },
|
|
|
|
|
{ label: "Workspace", value: "Project Storybook worktree", icon: FolderGit2 },
|
|
|
|
|
{ label: "Context", value: "visible before layout chrome", icon: Boxes },
|
|
|
|
|
].map((item) => {
|
|
|
|
|
const Icon = item.icon;
|
|
|
|
|
return (
|
|
|
|
|
<div key={item.label} className="rounded-lg border border-border bg-background p-4">
|
|
|
|
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
|
|
|
|
<div className="mt-3 text-xs uppercase tracking-[0.14em] text-muted-foreground">{item.label}</div>
|
|
|
|
|
<div className="mt-1 break-all font-mono text-xs">{item.value}</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ProjectsGoalsWorkspacesStories() {
|
|
|
|
|
return (
|
|
|
|
|
<StorybookData>
|
|
|
|
|
<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">Projects, goals, and workspaces</div>
|
|
|
|
|
<h1 className="mt-2 text-3xl font-semibold tracking-tight">Hierarchical planning and runtime surfaces</h1>
|
|
|
|
|
<p className="mt-3 max-w-3xl text-sm leading-6 text-muted-foreground">
|
|
|
|
|
Fixture-backed project and goal stories cover editable project properties, local and remote workspace
|
|
|
|
|
cards, cleanup failures, goal hierarchy states, and runtime command controls.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
<Badge variant="outline">active</Badge>
|
|
|
|
|
<Badge variant="outline">archived</Badge>
|
|
|
|
|
<Badge variant="outline">local workspace</Badge>
|
|
|
|
|
<Badge variant="outline">remote workspace</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<Section eyebrow="ProjectProperties" title="Full project detail panels with codebase, goals, env, and archive states">
|
|
|
|
|
<ProjectPropertiesMatrix />
|
|
|
|
|
</Section>
|
|
|
|
|
|
|
|
|
|
<Section eyebrow="ProjectWorkspacesContent" title="Workspace list with local, remote, cleanup-failed, and empty states">
|
|
|
|
|
<WorkspacesMatrix />
|
|
|
|
|
</Section>
|
|
|
|
|
|
|
|
|
|
<Section eyebrow="GoalProperties" title="Goal detail panel with progress and child goal context">
|
|
|
|
|
<GoalPropertiesMatrix />
|
|
|
|
|
</Section>
|
|
|
|
|
|
|
|
|
|
<Section eyebrow="GoalTree" title="Hierarchical goal tree with expand/collapse and progress sidecar">
|
|
|
|
|
<GoalTreeMatrix />
|
|
|
|
|
</Section>
|
|
|
|
|
|
|
|
|
|
<Section eyebrow="WorkspaceRuntimeControls" title="Runtime start, stop, restart, and disabled command states">
|
|
|
|
|
<RuntimeControlsMatrix />
|
|
|
|
|
</Section>
|
|
|
|
|
|
|
|
|
|
<Section eyebrow="WorktreeBanner" title="Worktree context banner with branch identity">
|
|
|
|
|
<WorktreeBannerMatrix />
|
|
|
|
|
</Section>
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
</StorybookData>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const meta = {
|
|
|
|
|
title: "Product/Projects Goals Workspaces",
|
|
|
|
|
component: ProjectsGoalsWorkspacesStories,
|
|
|
|
|
parameters: {
|
|
|
|
|
docs: {
|
|
|
|
|
description: {
|
|
|
|
|
component:
|
|
|
|
|
"Projects, goals, and workspaces stories cover project properties, workspace cards/lists, goal hierarchy panels, runtime controls, and worktree branding states.",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
} satisfies Meta<typeof ProjectsGoalsWorkspacesStories>;
|
|
|
|
|
|
|
|
|
|
export default meta;
|
|
|
|
|
|
|
|
|
|
type Story = StoryObj<typeof meta>;
|
|
|
|
|
|
|
|
|
|
export const SurfaceMatrix: Story = {};
|