Files
PaperClipAI/ui/src/components/PipelineLivenessBanner.tsx
T
DottaandPaperclip 43b005b704 Add pipeline workflow primitives and operator UI (#7903)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The pipeline subsystem models repeatable work as items moving
through stages, with agent automation, review gates, blockers, drift
notices, and linked work.
> - Operators need this to be usable as one coherent workflow surface,
not just as backend primitives or disconnected route experiments.
> - The branch now carries the pipeline data model, service/routes,
CLI/tutorial path, aggregation feeds, operator UI, stage automation
controls, liveness/retry handling, and follow-up polish that make the
primitive reviewable end to end.
> - This pull request is the single review target for that pipeline
workflow primitive stack.
> - The benefit is that reviewers can evaluate the full operator
experience and server contract together against `master`.

## Linked Issues or Issue Description

No public GitHub issue exists for this work. The underlying feature
request is described inline.

### Problem or motivation

Paperclip needs a first-class way to model multi-stage agent/company
workflows where upstream items can spawn downstream work, request
review, carry fields across pipelines, surface drift, retry automation,
and show operators where work is blocked or active. Without a unified
pipeline primitive, these workflows spread across ad hoc issues,
routines, and comments, making the state hard to inspect or operate.

### Proposed solution

Add the pipeline workflow primitive stack: database schema and
migrations, shared validators/types, server services and REST routes,
aggregation and liveness helpers, CLI/tutorial smoke support, and the
React operator UI for pipeline lists, boards, item detail,
review/learnings views, settings, stage automation, secrets, carry-over
fields, and retry/recovery flows.

### Alternatives considered

- Keep workflows as loosely linked issues and routines: rejected because
operators need a single board/detail/settings surface for repeated
workflow patterns.
- Ship backend primitives first and defer UI: rejected for this branch
because the operator experience is the main way to validate the
primitive.
- Add a narrower one-off content workflow: rejected because the same
primitives are useful across future company processes.

## What Changed

- Added and evolved pipeline schema, migrations, shared contracts,
server services, REST routes, route tests, and CLI/tutorial smoke
support.
- Added pipeline aggregation, health/liveness, drift acknowledgment,
blocker/carry-over, automation retry, stage automation environment, and
permission recovery behavior.
- Added the operator UI for pipeline index/board/item
detail/settings/review/learnings flows, including stage secrets,
automation controls, markdown/item descriptions, linked issue assets,
liveness banners, and source automation metadata.
- Refactored issue document frame rendering through the shared
`DocumentFrameHeader` component to keep document controls consistent
with the pipeline document surfaces.
- Kept this PR as the single base-branch review target for the current
pipeline branch.

## Verification

Current branch refresh:

- `pnpm vitest run server/src/__tests__/pipelines-service.test.ts` — 31
passed
- `pnpm vitest run server/src/__tests__/pipelines-routes.test.ts` — 19
passed
- `pnpm --filter ./server typecheck` — passed
- `pnpm --filter ./ui typecheck` — passed
- Verified Pipelines remains gated by `enablePipelines === true`:
sidebar item is hidden unless the flag is enabled, direct pipeline
routes redirect to `/dashboard` when disabled, and the Experimental
settings UI still has no Pipelines toggle.
- GitHub status checks on `df071c710646de625131064c3fb6588b5e97964a` —
all complete with no failing conclusions, including Actions, Socket,
Superagent/Security, and Greptile Review
- Greptile summary on `df071c710646de625131064c3fb6588b5e97964a` —
Confidence Score 5/5
- GitHub review-thread sweep — 0 unresolved Greptile threads

Previously recorded during branch development:

- Server pipeline service/route and aggregation tests
- Shared validator tests
- UI pipeline page/settings/item-detail/learnings/liveness tests
- Pipeline tutorial smoke path

## Risks

- High review surface: this is a large feature branch spanning database,
shared contracts, server behavior, CLI/docs, and UI.
- Migration ordering and schema compatibility need reviewer attention
because this branch has been kept current across multiple `master`
syncs.
- GitHub still reports merge state `BLOCKED` because the PR is awaiting
normal human review/branch-protection completion; all current status
checks are green.
- Branch-name checklist exception: this PR uses the pre-existing
requested branch name, which predates the current public-branch naming
rule. The PR title/body avoid internal issue references.

> 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 based on GPT-5, with repository tool use,
shell execution, git/GitHub CLI operations, and local verification
commands. Earlier commits in this branch were assisted by Paperclip
agents and other AI coding agents as recorded in commit authorship.

## 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 searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-06-26 12:02:44 -05:00

178 lines
6.3 KiB
TypeScript

import { AlertTriangle, ExternalLink, Loader2, Lock, RefreshCw } from "lucide-react";
import type { PipelineCaseLiveness } from "@paperclipai/shared";
import { Button } from "@/components/ui/button";
import { Link } from "@/lib/router";
import { cn } from "../lib/utils";
import { createIssueDetailPath } from "../lib/issueDetailBreadcrumb";
import {
derivePipelineLivenessBanner,
type LivenessBannerLink,
type LivenessBannerTone,
type LivenessRetryKind,
} from "../lib/pipeline-liveness";
interface TonePalette {
section: string;
icon: string;
pulse: string;
link: string;
button: string;
Icon: typeof AlertTriangle;
}
const TONE_PALETTES: Record<LivenessBannerTone, TonePalette> = {
blocked: {
section:
"border-amber-300 bg-amber-50 text-amber-950 dark:border-amber-900/70 dark:bg-amber-950/30 dark:text-amber-100",
icon: "text-amber-700 dark:text-amber-300",
pulse: "bg-amber-500",
link: "text-amber-900 dark:text-amber-100",
button:
"border-amber-300 bg-transparent hover:bg-amber-100 dark:border-amber-900/70 dark:hover:bg-amber-950/40",
Icon: AlertTriangle,
},
permission: {
section:
"border-purple-300 bg-purple-50 text-purple-950 dark:border-purple-900/70 dark:bg-purple-950/30 dark:text-purple-100",
icon: "text-purple-700 dark:text-purple-300",
pulse: "bg-purple-500",
link: "text-purple-900 dark:text-purple-100",
button:
"border-purple-300 bg-transparent hover:bg-purple-100 dark:border-purple-900/70 dark:hover:bg-purple-950/40",
Icon: Lock,
},
retry: {
section:
"border-indigo-300 bg-indigo-50 text-indigo-950 dark:border-indigo-900/70 dark:bg-indigo-950/30 dark:text-indigo-100",
icon: "text-indigo-700 dark:text-indigo-300",
pulse: "bg-indigo-500",
link: "text-indigo-900 dark:text-indigo-100",
button:
"border-indigo-300 bg-transparent hover:bg-indigo-100 dark:border-indigo-900/70 dark:hover:bg-indigo-950/40",
Icon: RefreshCw,
},
attention: {
section:
"border-orange-300 bg-orange-50 text-orange-950 dark:border-orange-900/70 dark:bg-orange-950/30 dark:text-orange-100",
icon: "text-orange-700 dark:text-orange-300",
pulse: "bg-orange-500",
link: "text-orange-900 dark:text-orange-100",
button:
"border-orange-300 bg-transparent hover:bg-orange-100 dark:border-orange-900/70 dark:hover:bg-orange-950/40",
Icon: AlertTriangle,
},
};
function blockerLinkLabel(link: LivenessBannerLink): string {
if (link.identifier) return `Open ${link.identifier}`;
return "Open blocker";
}
function automationLinkLabel(link: LivenessBannerLink): string {
if (link.identifier) return `Open ${link.identifier}`;
return "Open automation task";
}
export function PipelineLivenessBanner({
liveness,
onRetry,
retryPending = false,
retryError = null,
}: {
liveness: PipelineCaseLiveness | null | undefined;
onRetry?: (kind: LivenessRetryKind) => void;
retryPending?: boolean;
retryError?: string | null;
}) {
const view = derivePipelineLivenessBanner(liveness);
if (!view) return null;
const palette = TONE_PALETTES[view.tone];
const { Icon } = palette;
const showRetry = view.showRetry && typeof onRetry === "function";
return (
<section
role="status"
aria-label={view.title}
className={cn(
"mb-5 flex flex-col gap-3 border-y py-4 md:flex-row md:items-start md:justify-between",
palette.section,
)}
>
<div className="flex min-w-0 gap-3">
<Icon className={cn("mt-0.5 h-4 w-4 shrink-0", palette.icon)} aria-hidden="true" />
<div className="min-w-0 space-y-1">
<h2 className="flex items-center gap-2 text-sm font-semibold">
{view.tone === "retry" ? (
<span
className={cn("h-1.5 w-1.5 animate-pulse rounded-full", palette.pulse)}
aria-hidden="true"
/>
) : null}
{view.title}
</h2>
<p className="text-sm opacity-85">{view.body}</p>
{view.permissionKey ? (
<p className="text-sm opacity-85">
Required permission:{" "}
<code className="rounded-sm bg-black/10 px-1 py-0.5 text-xs font-medium dark:bg-white/10">
{view.permissionKey}
</code>{" "}
on the target pipeline.
</p>
) : null}
{view.blockerLink || view.automationLink ? (
<p className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm">
{view.blockerLink ? (
<Link
to={createIssueDetailPath(view.blockerLink.identifier ?? view.blockerLink.issueId)}
className={cn("inline-flex items-center gap-1 font-medium underline-offset-2 hover:underline", palette.link)}
>
<ExternalLink className="h-3.5 w-3.5" />
{blockerLinkLabel(view.blockerLink)}
{view.blockerLink.title ? `: ${view.blockerLink.title}` : ""}
</Link>
) : null}
{view.automationLink ? (
<Link
to={createIssueDetailPath(view.automationLink.identifier ?? view.automationLink.issueId)}
className={cn("inline-flex items-center gap-1 font-medium underline-offset-2 hover:underline", palette.link)}
>
<ExternalLink className="h-3.5 w-3.5" />
{automationLinkLabel(view.automationLink)}
</Link>
) : null}
</p>
) : null}
{view.helperNote ? (
<p className="text-xs italic opacity-70">{view.helperNote}</p>
) : null}
{retryError ? (
<p role="alert" className="text-sm font-medium text-destructive">
{retryError}
</p>
) : null}
</div>
</div>
{showRetry ? (
<Button
type="button"
size="sm"
variant="outline"
className={cn("shrink-0", palette.button)}
disabled={retryPending}
onClick={() => onRetry?.(view.retryKind)}
>
{retryPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<RefreshCw className="mr-2 h-4 w-4" />
)}
{retryPending ? "Retrying…" : view.retryLabel}
</Button>
) : null}
</section>
);
}