diff --git a/.agents/skills/constructive-blocks/SKILL.md b/.agents/skills/constructive-blocks/SKILL.md index b4126e49..37c08133 100644 --- a/.agents/skills/constructive-blocks/SKILL.md +++ b/.agents/skills/constructive-blocks/SKILL.md @@ -101,6 +101,7 @@ to endpoints, sessions, adapters, or tenant data. | Application shell (sidebar rail, drawer, views) | `workspace-kit` | | AI or agent UI | `ai` | | Complete agent workspace (chat, integrations, skills, agent canvas) | `agents-builder` | +| Coding agent workspace (parallel sessions, diff review, file and browser tabs, terminal) | `coding-agent` | | Secure document rooms shared inside and outside an organization (roles, folder shares, agreements, Q&A, insights) | `data-rooms` | | Global commands and workflows | `command-palette` | | Metadata-driven data grid | `sheets` | diff --git a/.agents/skills/constructive-blocks/references/surfaces-and-ownership.md b/.agents/skills/constructive-blocks/references/surfaces-and-ownership.md index 8e8b93ee..892fbc50 100644 --- a/.agents/skills/constructive-blocks/references/surfaces-and-ownership.md +++ b/.agents/skills/constructive-blocks/references/surfaces-and-ownership.md @@ -11,6 +11,7 @@ live `shadcn@latest search` and `view`. | Button, form, overlay, navigation, or layout | A primitive name, `form-kit`, `overlay-kit`, `layout-kit`, `workspace-kit` | Installed source owns presentation; the host owns application behavior. | | AI chat or agent traces | `ai` | Presentational only. The host owns the model runtime, streaming reducer, tools, IPC, and persistence. | | Agent workspace template | `agents-builder` | Installed source owns the chat, integrations, skills, and agent-canvas views plus scripted playback; the host supplies workspace data and owns OAuth, persistence, routing, and model runs. | +| Coding agent workspace | `coding-agent` | Installed source owns session tabs and the sidebar layout, the transcript and composer, diff review with line comments, file and browser tabs, the terminal dock, and scripted playback; the host supplies projects and sessions and owns the agent runtime, the checkout, shell commands, browser previews, and persistence. | | Secure document rooms | `data-rooms` | Installed source owns the rooms, documents, people, Q&A, insights, settings, and access-map views, the access explanations and previews, optimistic local updates, and scripted assistant answers; the host supplies workspace data and owns storage, delivery, authorization, the activity log, and assistant runs. | | Searchable application commands | `command-palette` | The npm package owns the headless engine; installed source owns the dialog and task presentation; the host owns routing and actions. | | Arbitrary application-table CRUD | `sheets` | Installed source owns the grid; the data runtime interprets current `_meta`; the host owns endpoint and session discovery. | @@ -72,6 +73,7 @@ and dependency closure. The public documentation provides richer examples: - Org Chart: `https://constructive-io.github.io/blocks/blocks/org-chart/` - Storage: `https://constructive-io.github.io/blocks/blocks/storage/` (Storage Browser at `/blocks/storage/browser/`, the feature pack at `/blocks/features/storage/`) - Agents Builder: `https://constructive-io.github.io/blocks/blocks/agents-builder/` +- Coding Agent: `https://constructive-io.github.io/blocks/blocks/coding-agent/` - Data Rooms: `https://constructive-io.github.io/blocks/blocks/data-rooms/` - Billing: `https://constructive-io.github.io/blocks/blocks/billing/` (Billing Account at `/blocks/billing/account/`, Billing Console at `/blocks/billing/console/`) - Feature packs: `https://constructive-io.github.io/blocks/blocks/features/` diff --git a/AGENTS.md b/AGENTS.md index 148eefd0..412f6721 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,7 +55,7 @@ Use Node 24 LTS and pnpm 10.28.0, then run `pnpm check`, `pnpm build:pages`, and on `motion/react` or CSS, and keep npm package peers usable on React 18. jsdom lacks `CSS.escape`, which React's ViewTransition calls; polyfill it in tests that render these boundaries. -- Workspace templates (`agents-builder`, `billing-account`, `billing-console`) +- Workspace templates (`agents-builder`, `coding-agent`, `billing-account`, `billing-console`) share `packages/ui/src/components/workspace-kit` (shell, sidebar rail and drawer, view frames, filter pills, surfaces such as `Panel` and `TableSurface`, and the pan-and-zoom canvas used by Agents Builder and diff --git a/apps/blocks/src/app/blocks/coding-agent/page.tsx b/apps/blocks/src/app/blocks/coding-agent/page.tsx new file mode 100644 index 00000000..d32d0694 --- /dev/null +++ b/apps/blocks/src/app/blocks/coding-agent/page.tsx @@ -0,0 +1,23 @@ +import type { Metadata } from 'next'; + +import { ApplicationBlockDocsPage } from '@/components/application-block-showcase/application-block-docs-page'; +import { getApplicationBlock } from '@/lib/application-blocks'; +import { OG_IMAGE, withBase } from '@/lib/site'; + +const block = getApplicationBlock('coding-agent')!; + +export default function CodingAgentPage() { + return ; +} + +export const metadata: Metadata = { + title: block.title, + description: block.description, + alternates: { canonical: withBase('/blocks/coding-agent') }, + openGraph: { + title: block.title, + description: block.description, + url: withBase('/blocks/coding-agent'), + images: [OG_IMAGE], + }, +}; diff --git a/apps/blocks/src/app/blocks/coding-agent/preview/page.tsx b/apps/blocks/src/app/blocks/coding-agent/preview/page.tsx new file mode 100644 index 00000000..b7d326f2 --- /dev/null +++ b/apps/blocks/src/app/blocks/coding-agent/preview/page.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from 'next'; +import { Suspense } from 'react'; + +import { ApplicationBlockShowcaseCanvas } from '@/components/application-block-showcase/application-block-showcase-canvas'; +import { withBase } from '@/lib/site'; + +export default function CodingAgentPreviewPage() { + return ( + <> +

Coding Agent live preview

+ {/* The preview reads its example from the query string, which only exists in the browser. */} + }> + + + + ); +} + +export const metadata: Metadata = { + alternates: { canonical: withBase('/blocks/coding-agent') }, + robots: { follow: false, index: false }, +}; diff --git a/apps/blocks/src/app/globals.css b/apps/blocks/src/app/globals.css index 49e3af7b..82ed3799 100644 --- a/apps/blocks/src/app/globals.css +++ b/apps/blocks/src/app/globals.css @@ -7,6 +7,7 @@ @source '../'; @source '../../../../packages/ui/src/components/agents-builder'; +@source '../../../../packages/ui/src/components/coding-agent'; @source '../../../../packages/ui/src/components/data-rooms'; @source '../../../../packages/ui/src/components/workspace-kit'; @source '../../../../packages/ui/src/components/org-chart'; diff --git a/apps/blocks/src/components/application-block-showcase/application-block-docs-page.tsx b/apps/blocks/src/components/application-block-showcase/application-block-docs-page.tsx index bb162120..04fea1bd 100644 --- a/apps/blocks/src/components/application-block-showcase/application-block-docs-page.tsx +++ b/apps/blocks/src/components/application-block-showcase/application-block-docs-page.tsx @@ -129,6 +129,14 @@ export function ApplicationBlockDocsPage({ + {block.guides?.map((guide) => ( + + + {guide.example} + + + ))} + - {block.buildingBlocks?.length ? ( - + {block.buildingBlocks ? ( + @@ -170,6 +174,12 @@ export function ApplicationBlockDocsPage({ > `${example.label}: ${example.description}`), + ] + : []), 'Use the breakpoint controls to inspect the same source at desktop, tablet, and mobile widths.', 'Open the full-screen preview when you need to test menus, sheets, panning, or dense table interactions without documentation chrome.', ]} diff --git a/apps/blocks/src/components/application-block-showcase/application-block-showcase-canvas.tsx b/apps/blocks/src/components/application-block-showcase/application-block-showcase-canvas.tsx index a105f197..58c66f51 100644 --- a/apps/blocks/src/components/application-block-showcase/application-block-showcase-canvas.tsx +++ b/apps/blocks/src/components/application-block-showcase/application-block-showcase-canvas.tsx @@ -8,6 +8,9 @@ import type { ApplicationBlockDoc } from '@/lib/application-blocks'; const AgentsBuilderPreview = dynamic(() => import('./agents-builder-preview').then((module) => module.AgentsBuilderPreview), ); +const CodingAgentPreview = dynamic(() => + import('./coding-agent-preview').then((module) => module.CodingAgentPreview), +); const DataRoomsPreview = dynamic(() => import('./data-rooms-preview').then((module) => module.DataRoomsPreview), ); @@ -30,6 +33,7 @@ export function ApplicationBlockShowcaseCanvas({ name: ApplicationBlockDoc['name']; }) { if (name === 'agents-builder') return ; + if (name === 'coding-agent') return ; if (name === 'data-rooms') return ; if (name === 'billing-account') return ; if (name === 'billing-console') return ; diff --git a/apps/blocks/src/components/application-block-showcase/application-block-showcase-preview.tsx b/apps/blocks/src/components/application-block-showcase/application-block-showcase-preview.tsx index f4bc7289..a8dbecde 100644 --- a/apps/blocks/src/components/application-block-showcase/application-block-showcase-preview.tsx +++ b/apps/blocks/src/components/application-block-showcase/application-block-showcase-preview.tsx @@ -1,13 +1,20 @@ 'use client'; +import { useId, useState } from 'react'; + import { Badge } from '@constructive-io/ui/badge'; +import { Field } from '@constructive-io/ui/field'; +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@constructive-io/ui/select'; import { LivePreview } from '@/components/docs/live-preview'; +import type { ApplicationBlockExample } from '@/lib/application-blocks'; export type LivePreviewBlock = Readonly<{ previewDescription: string; previewHeight: number; title: string; + /** Named ways to open the preview, passed to the preview route as `?example=`. The first is the default. */ + examples?: readonly ApplicationBlockExample[]; }>; export function ApplicationBlockShowcasePreview({ @@ -17,8 +24,38 @@ export function ApplicationBlockShowcasePreview({ block: LivePreviewBlock; previewPath: string; }) { + const controlId = useId(); + const examples = block.examples ?? []; + const [example, setExample] = useState(examples[0]?.value); + const selected = examples.find((option) => option.value === example); + return ( 1 ? ( + + + + ) : undefined + } defaultViewport="mobile" frameSlot="application-block-preview-frame" height={block.previewHeight} @@ -26,7 +63,7 @@ export function ApplicationBlockShowcasePreview({ name={block.title} responsive slot="application-block-showcase-preview" - src={previewPath} + src={example ? `${previewPath}?example=${encodeURIComponent(example)}` : previewPath} /> ); } diff --git a/apps/blocks/src/components/application-block-showcase/coding-agent-preview.tsx b/apps/blocks/src/components/application-block-showcase/coding-agent-preview.tsx new file mode 100644 index 00000000..ecbbcdd8 --- /dev/null +++ b/apps/blocks/src/components/application-block-showcase/coding-agent-preview.tsx @@ -0,0 +1,103 @@ +'use client'; + +import { useSearchParams } from 'next/navigation'; +import { useState } from 'react'; + +import { + CODING_AGENT_DEMO, + CodingAgent, + type CodingAgentAction, + type CodingAgentLayout, +} from '@/components/ui/coding-agent'; +import type { CodingAgentExample } from '@/lib/application-blocks/coding-agent'; + +/** The docs preview's examples, picked with `?example=`; each opens the same demo data a different way. */ +const EXAMPLES: Record = { + tabs: { + layout: 'tabs', + sessionId: 'session-invites', + hint: 'Watch the first session run, approve its test command, review the diff, or answer the question waiting in another tab.', + }, + sidebar: { + layout: 'sidebar', + sessionId: 'session-invites', + hint: 'Sessions sit under their project with their live status; collapse the sidebar to a rail or switch back to tabs from its footer.', + }, + 'sub-agents': { + layout: 'tabs', + sessionId: 'session-security', + hint: 'The lead agent runs three workers in parallel: open one from its card, approve the command one of them asks for, or follow them in the Agents tab.', + }, +}; + +function describeCodingAgentAction(action: CodingAgentAction) { + switch (action.type) { + case 'new-session': + return `The host would start a session ${action.location === 'worktree' ? 'in a new worktree' : 'in the checkout'}.`; + case 'send': + return 'The host would send the message to the agent runtime.'; + case 'stop': + return 'The host would stop the running agent.'; + case 'stop-run': + return 'The host would stop that agent and everything it started.'; + case 'pause-run': + case 'resume-run': + return `The host would ${action.type === 'pause-run' ? 'pause' : 'resume'} that agent.`; + case 'steer-run': + return 'The host would pass the message to that agent mid-run.'; + case 'retry-run': + return 'The host would run that agent again.'; + case 'permission': + return action.decision === 'deny' + ? `The host would tell the agent not to run ${action.command}.` + : `The host would let the agent run ${action.command}.`; + case 'open-pull-request': + return `The host would open pull request #${action.number}.`; + case 'create-pull-request': + return `The host would open a pull request for ${action.worktreeId}.`; + case 'rerun-checks': + return `The host would re-run ${action.checkIds.length === 1 ? 'the failed check' : `${action.checkIds.length} failed checks`} on ${action.worktreeId}.`; + case 'answer': + return 'The host would hand the answer back to the waiting agent.'; + case 'comment': + return `The host would attach a comment on ${action.path}:${action.line} to the next message.`; + case 'terminal-command': + return `The host would run ${action.command} in the session's shell.`; + case 'open-file': + return `The host would read ${action.path} from the checkout.`; + case 'open-url': + return `The host would load ${action.url} in the browser tab.`; + case 'layout': + return `Sessions now show as ${action.layout === 'tabs' ? 'tabs' : 'a sidebar'}.`; + default: + return `The host received ${action.type}.`; + } +} + +export function CodingAgentPreview() { + const requested = useSearchParams().get('example'); + const example = requested && requested in EXAMPLES ? EXAMPLES[requested as CodingAgentExample] : EXAMPLES.tabs; + const [message, setMessage] = useState(example.hint); + + return ( +
+ setMessage(describeCodingAgentAction(action))} + /> +

+ {message} +

+
+ ); +} diff --git a/apps/blocks/src/components/site/registry-shell.tsx b/apps/blocks/src/components/site/registry-shell.tsx index 57185025..4b8c0e53 100644 --- a/apps/blocks/src/components/site/registry-shell.tsx +++ b/apps/blocks/src/components/site/registry-shell.tsx @@ -41,7 +41,7 @@ export function RegistryShell({ children }: { children: ReactNode }) { const isConsoleKitProof = pathname === '/__integration/console-kit'; const isStandalonePreview = isConsoleKitProof || - /^\/blocks\/(?:features\/[^/]+|org-chart|storage\/browser|agents-builder|data-rooms|billing\/(?:account|console)|sheets|schema-builder|documents|create)\/preview\/?$/.test(pathname); + /^\/blocks\/(?:features\/[^/]+|org-chart|storage\/browser|agents-builder|coding-agent|data-rooms|billing\/(?:account|console)|sheets|schema-builder|documents|create)\/preview\/?$/.test(pathname); const menuButtonRef = useRef(null); const sidebarRef = useRef(null); const previouslyFocusedRef = useRef(null); diff --git a/apps/blocks/src/lib/application-blocks.ts b/apps/blocks/src/lib/application-blocks.ts deleted file mode 100644 index a5eb94d5..00000000 --- a/apps/blocks/src/lib/application-blocks.ts +++ /dev/null @@ -1,874 +0,0 @@ -import type { DocSectionId } from './doc-sections'; - -export type ApplicationBlockApiRow = Readonly<{ - name: string; - type: string; - behavior: string; -}>; - -export type ApplicationBlockDoc = Readonly<{ - name: 'org-chart' | 'storage-browser' | 'agents-builder' | 'data-rooms' | 'billing-account' | 'billing-console'; - /** Docs route when it is not `/blocks/`, e.g. inside a section such as Billing. */ - href?: string; - /** Section hub the block is listed under instead of its own nav item. */ - section?: DocSectionId; - title: string; - description: string; - previewDescription: string; - previewHeight: number; - whenToUse: readonly string[]; - usage: Readonly<{ - description: string; - example: string; - }>; - state: Readonly<{ - title: string; - description: string; - }>; - composition: readonly string[]; - accessibility: readonly string[]; - api: readonly ApplicationBlockApiRow[]; - /** Leaf components the template is built from, each usable on its own. */ - buildingBlocks?: readonly ApplicationBlockApiRow[]; -}>; - -export const APPLICATION_BLOCKS: readonly ApplicationBlockDoc[] = [ - { - name: 'org-chart', - title: 'Org Chart', - description: - 'An interactive organization chart on the workspace canvas: automatic top-down layout, pan and zoom, folding teams, a details panel, and drag-to-reassign with a keyboard alternative.', - previewDescription: - 'Select a person to see their manager and reports, fold a team away, or drag a card onto a new manager and watch the preview wire follow.', - previewHeight: 740, - whenToUse: [ - 'Use Org Chart when reporting relationships are central to understanding or managing an organization.', - 'Use a table or tree when dense scanning, bulk editing, or keyboard-only hierarchy management is the primary task.', - ], - usage: { - description: - 'Pass either controlled edges or defaultEdges. The component owns viewport and selection behavior while the host owns persistence and application workflows.', - example: `'use client'; - -import { - OrgChart, - type OrgChartEdge -} from '@/components/ui/org-chart'; - -const reportingLines: OrgChartEdge[] = [ - { - id: 'alex', - parentId: null, - displayName: 'Alex Morgan', - positionTitle: 'Chief Executive Officer', - avatarUrl: null - }, - { - id: 'sam', - parentId: 'alex', - displayName: 'Sam Rivera', - positionTitle: 'VP of Product', - avatarUrl: null - } -]; - -type ReportingLineInput = Readonly<{ - personId: string; - managerId: string; - positionTitle?: string | null; -}>; - -type CompanyOrgChartProps = Readonly<{ - saveReportingLine: ( - reportingLine: ReportingLineInput - ) => void | Promise; - openPositionEditor: (personId: string) => void; - openRemovalConfirmation: (personId: string) => void; -}>; - -export function CompanyOrgChart({ - saveReportingLine, - openPositionEditor, - openRemovalConfirmation -}: CompanyOrgChartProps) { - return ( - - saveReportingLine({ - personId, - managerId, - positionTitle: preserve.positionTitle - }) - } - onEditNode={(person) => openPositionEditor(person.id)} - onRemoveNode={(person) => openRemovalConfirmation(person.id)} - /> - ); -}`, - }, - state: { - title: 'Hierarchy and interaction state', - description: - 'Use edges when the host owns the current hierarchy. Use defaultEdges for optimistic local reparenting, and persist each accepted move through onReparent.', - }, - composition: [ - 'The workspace canvas (shared with Agents Builder) owns pan, pinch, wheel, and keyboard zoom; the block lays flat reporting edges out as a tidy top-down tree and draws bezier wires between cards. More than eight reports without a team of their own stack in columns of up to eight under their manager instead of running in one row.', - 'Selecting a person highlights their reporting chain and opens a details panel with their manager, direct reports, and actions; folded teams show a stacked card edge and the team size.', - 'Node menus delegate editing and removal to the host, so the chart never assumes a router, form system, or destructive-action policy.', - 'Loading and empty states are built in, while success and error messaging remain injectable through observer callbacks.', - ], - accessibility: [ - 'Keep every displayName and positionTitle meaningful because the same labels identify node actions and reporting relationships.', - 'The chart is an ARIA tree: Tab into it, move between people with the arrow keys (inside a stacked column, Up and Down move through the column and Left and Right move between columns), press Enter for details, and use Change manager to reassign without dragging.', - 'Confirm removal in the host workflow before changing data; the node menu reports intent and does not delete records itself.', - ], - api: [ - { - name: 'edges / defaultEdges', - type: 'OrgChartEdge[]', - behavior: - 'Chooses controlled or uncontrolled hierarchy state. Supply exactly one of these properties.', - }, - { - name: 'editable', - type: 'boolean', - behavior: - 'Enables dragging and node actions. Defaults to true.', - }, - { - name: 'isLoading', - type: 'boolean', - behavior: - 'Replaces the chart with a bounded loading surface while hierarchy data is pending.', - }, - { - name: 'onReparent', - type: '(childId, newParentId, preserve) => void | Promise', - behavior: - 'Persists a valid reporting-line change. Uncontrolled mode reverts its optimistic move when the promise rejects.', - }, - { - name: 'onAddToChart / onEditNode / onRemoveNode', - type: 'Callbacks', - behavior: - 'Delegates creation, editing, and removal workflows without coupling the chart to dialogs or routing.', - }, - { - name: 'onReparentSuccess / onReparentError', - type: 'Observer callbacks', - behavior: - 'Lets the host present localized feedback for accepted and rejected reporting-line changes.', - }, - { - name: 'showDetails / onSelectNode', - type: 'boolean / (node | null) => void', - behavior: - 'Shows the built-in details panel for the selected person (default true), and reports selection so a host can render its own panel instead.', - }, - { - name: 'defaultCollapsedIds', - type: 'string[]', - behavior: - 'People whose teams start folded away, useful for large organizations.', - }, - { - name: 'className', - type: 'string', - behavior: - 'Adds layout classes to the chart container; use a height utility to replace the default 600-pixel canvas.', - }, - ], - }, - { - name: 'storage-browser', - href: '/blocks/storage/browser', - section: 'storage', - title: 'Storage Browser', - description: - 'A controlled storage workspace: buckets in the workspace sidebar, a file list with type-tinted glyphs, search, sort, drag-and-drop upload, a floating selection bar, detail and bucket sheets, and distinct empty states.', - previewDescription: - 'Switch buckets, search and sort files, select a few to see the selection bar, drop files onto the list, or open one for its preview and details.', - previewHeight: 720, - whenToUse: [ - 'Use Storage Browser when people need to inspect and manage objects across several application buckets.', - 'Install a focused storage leaf when the surrounding product already owns navigation, tables, or configuration surfaces.', - ], - usage: { - description: - 'Map GraphQL results into the storage domain types and keep selection, search, sort, folders, and actions in the host. The browser performs no fetching or authorization checks.', - example: `'use client'; - -import { useMemo, useState } from 'react'; - -import { - ObjectDetailSheet, - StorageBrowser, - type ObjectSort, - type StorageBucket, - type StorageObject -} from '@/components/ui/storage'; - -type AssetBrowserActions = Readonly<{ - confirmDelete: (objectIds: string[]) => void; - copyLink: (object: StorageObject) => void; - createBucket: () => void; - download: (object: StorageObject) => void; - rename: (objectId: string, filename?: string) => void; - upload: () => void; -}>; - -function compareObjects( - left: StorageObject, - right: StorageObject, - sort: ObjectSort -) { - let comparison = 0; - if (sort.column === 'filename') { - comparison = (left.filename ?? left.key).localeCompare( - right.filename ?? right.key - ); - } else if (sort.column === 'mimeType') { - comparison = left.mimeType.localeCompare(right.mimeType); - } else if (sort.column === 'size') { - comparison = left.size - right.size; - } else { - comparison = - new Date(left.createdAt).getTime() - - new Date(right.createdAt).getTime(); - } - return sort.direction === 'asc' ? comparison : -comparison; -} - -export function AssetBrowser({ - actions, - buckets, - objects -}: Readonly<{ - actions: AssetBrowserActions; - buckets: StorageBucket[]; - objects: StorageObject[]; -}>) { - const [bucketId, setBucketId] = useState( - buckets[0]?.id ?? null - ); - const [openedObject, setOpenedObject] = useState(null); - const [query, setQuery] = useState(''); - const [selectedIds, setSelectedIds] = useState([]); - const [sort, setSort] = useState({ - column: 'createdAt', - direction: 'desc' - }); - const visibleObjects = useMemo(() => { - const search = query.trim().toLocaleLowerCase(); - return objects - .filter((object) => { - if (object.bucketId !== bucketId) return false; - if (!search) return true; - const searchable = [ - object.filename ?? '', - object.key, - object.mimeType - ].join(' '); - return searchable.toLocaleLowerCase().includes(search); - }) - .sort((left, right) => compareObjects(left, right, sort)); - }, [bucketId, objects, query, sort]); - - return ( - <> - setSelectedIds([])} - onCopyLink={actions.copyLink} - onDelete={(object) => actions.confirmDelete([object.id])} - onDownload={actions.download} - onNewBucket={actions.createBucket} - onOpenObject={setOpenedObject} - onQueryChange={setQuery} - onRename={(object) => actions.rename(object.id)} - onSelectBucket={(nextBucketId) => { - setBucketId(nextBucketId); - setSelectedIds([]); - }} - onSelectionChange={setSelectedIds} - onSortChange={setSort} - onUpload={actions.upload} - /> - { - actions.confirmDelete([objectId]); - setOpenedObject(null); - }} - onDownload={actions.download} - onOpenChange={(open) => { - if (!open) setOpenedObject(null); - }} - onRename={actions.rename} - /> - - ); -}`, - }, - state: { - title: 'Data and action boundaries', - description: - 'Every collection, selection, filter, sort, and mutation callback is controlled. Filter and authorize records before passing them to the block, then refresh its props after a mutation succeeds.', - }, - composition: [ - 'The storage-browser registry item installs the shared storage barrel and every leaf: bucket rail, object table, upload dropzone, detail sheet, bucket configuration sheet, and empty states.', - 'StorageBrowser sits on the shared workspace shell (the same sidebar rail, drawer, and view header as Agents Builder and Billing) and composes the bucket rail, folder path, search, sort menu, object table, selection bar, and empty states; render the installed sheets and upload surface beside it.', - 'The types mirror Constructive storage records, but the UI remains transport-neutral and does not bypass PostgreSQL privileges or RLS.', - ], - accessibility: [ - 'Preserve object filenames, MIME types, sizes, and dates as text so file state never depends on an icon or color alone.', - 'Keep selection controlled and announce the resulting count; bulk deletion should open a confirmation before calling the mutation.', - 'Treat unavailable storage, denied access, no buckets, and an empty bucket as distinct states because they require different recovery actions.', - ], - api: [ - { - name: 'buckets / selectedBucketId / onSelectBucket', - type: 'StorageBucket[] / string | null / callback', - behavior: - 'Supplies the bucket rail and keeps the active bucket controlled by the host.', - }, - { - name: 'objects / selectedIds / onSelectionChange', - type: 'StorageObject[] / string[] / callback', - behavior: - 'Supplies visible objects and controls row and bulk selection.', - }, - { - name: 'sort / onSortChange', - type: 'ObjectSort / callback', - behavior: - 'Reports the requested column and direction; the host returns objects in the corresponding order.', - }, - { - name: 'query / onQueryChange', - type: 'string / callback', - behavior: - 'Controls the search field while filtering or remote querying remains in the host.', - }, - { - name: 'segments / onNavigate', - type: 'StorageBreadcrumbSegment[] / callback', - behavior: - 'Adds optional folder navigation beneath the active bucket header.', - }, - { - name: 'onUpload / onBulkDelete / object callbacks', - type: 'Callbacks', - behavior: - 'Delegates upload, delete, open, download, copy-link, rename, and row-delete workflows.', - }, - { - name: 'onDropFiles', - type: '(files: FileList) => void', - behavior: - 'Turns the file list into a drop target: dragging files over it shows where they will land, and dropping hands the files to the host.', - }, - { - name: 'locale', - type: 'string', - behavior: - 'Formats the counts in the rail, toolbar and selection bar with the reader\'s own separators (for example "de-DE"). Counts render with "en-US" separators by default so the server and browser agree.', - }, - { - name: 'title / defaultSidebarCollapsed', - type: 'string / boolean', - behavior: - 'Names the workspace in the sidebar header (default "Storage") and starts the sidebar as an icon rail.', - }, - { - name: 'isLoading / emptyState / emptyLabel', - type: 'boolean / StorageEmptyStateVariant / string', - behavior: - 'Distinguishes pending results, empty table results, and full-pane storage states.', - }, - { - name: 'bulkDeleteProgress', - type: '{ done: number; total: number; failed: string[] }', - behavior: - 'Shows deterministic progress and partial failures while a host-owned bulk deletion is running.', - }, - { - name: 'className', - type: 'string', - behavior: - 'Adds layout classes to the outer storage workspace.', - }, - ], - }, - { - name: 'agents-builder', - title: 'Agents Builder', - description: - 'A complete agent workspace template: chat with inline questions and tool traces, an integrations directory with a two-step connect flow, a skills library, and an agent detail view with a live run panel and pannable canvas.', - previewDescription: - 'Start a recommended chat, answer the agent’s questions, connect a source, triage agent-drafted replies in the Inbox, pause or run schedules, then open Revenue Analyst to watch a run reason, query the warehouse, fan out across sub-agents and skills, and wait for your approval.', - previewHeight: 820, - whenToUse: [ - 'Use Agents Builder as the starting shell for a product where people chat with agents, connect the apps those agents read from, and inspect what each agent runs.', - 'Install the AI kit primitives on their own when you only need a chat surface or a single agent card inside an existing layout.', - ], - usage: { - description: - 'Pass workspace data and handle side effects through callbacks. The template owns navigation between its views, the connect dialog, and scripted playback; the host owns routing, OAuth, persistence, and model calls.', - example: `'use client'; - -import { useRouter } from 'next/navigation'; - -import { - AgentsBuilder, - type AgentsBuilderAction, - type AgentsBuilderData, - type Integration -} from '@/components/ui/agents-builder'; - -type AgentWorkspaceProps = Readonly<{ - data: AgentsBuilderData; - startOAuth: (integration: Integration) => Promise; - saveConnection: ( - integrationId: string, - toolIds: string[] - ) => Promise; -}>; - -export function AgentWorkspace({ - data, - startOAuth, - saveConnection -}: AgentWorkspaceProps) { - const router = useRouter(); - - const handleAction = (action: AgentsBuilderAction) => { - if (action.type === 'navigate') router.push(\`/\${action.target}\`); - if (action.type === 'open-agent') router.push(\`/agents/\${action.agentId}\`); - }; - - return ( -
- - saveConnection(integration.id, toolIds) - } - onAction={handleAction} - /> -
- ); -}`, - }, - state: { - title: 'Views, connections, and playback', - description: - 'The active view is uncontrolled by default; pass view and onViewChange to sync it with a router. Connections start from each integration’s connected flag and update only after onConnectIntegration resolves. Chat replies and the agent run play from scripted beats, so map live model and run events onto the same shapes when you connect a runtime.', - }, - composition: [ - 'Built from the AI kit: PromptInput, ChatContainer, ToolTrace, ThinkingStatus, AskCard, ConnectPrompt, AgentDraftCard, and UsageNotice, so each surface can be reused outside the template.', - 'Each view (ChatView, IntegrationsView, SkillsView, AgentView) and the ConnectIntegrationDialog is exported for hosts that want their own shell or router.', - 'Integration marks fall back to tinted monograms; pass real brand marks through each integration’s mark field.', - ], - accessibility: [ - 'Every icon-only control has a label and a tooltip; the collapsed sidebar keeps labels available through tooltips and aria-label.', - 'The agent canvas is a focusable region: arrow keys pan, plus and minus zoom, and zero resets to 100%.', - 'Clarifying questions use radio semantics with lettered options and a labeled free-text row, and streamed status lines are announced politely.', - 'Scripted playback shortens to near-instant steps when reduced motion is requested.', - ], - api: [ - { - name: 'data', - type: 'AgentsBuilderData', - behavior: - 'Workspace name, models, integrations and categories, agents, skills, starter packs, chat recommendations and replies, the agent blueprint, and its run script. AGENTS_BUILDER_DEMO is a complete example.', - }, - { - name: 'view / defaultView / onViewChange', - type: "'chat' | 'integrations' | 'skills' | 'agent'", - behavior: 'Controls or seeds the active view.', - }, - { - name: 'onAuthorizeIntegration', - type: '(integration) => Promise', - behavior: - 'Runs the provider consent handoff. Resolve to advance to tool selection; reject to return to sign-in.', - }, - { - name: 'onConnectIntegration', - type: '(integration, toolIds) => void | Promise', - behavior: - 'Persists a connection with the tools left enabled. Rejecting keeps the dialog open on the tools step.', - }, - { - name: 'onAction', - type: '(action: AgentsBuilderAction) => void', - behavior: - 'Receives every host-owned control: navigation, search, sharing, skill and agent links, workspace menu items, stop, and follow-ups.', - }, - { - name: 'theme / onThemeChange', - type: "'light' | 'dark' | 'system'", - behavior: 'Drives the appearance switch in the workspace menu.', - }, - { - name: 'defaultSidebarCollapsed / autoplayRun', - type: 'boolean', - behavior: - 'Starts with the icon rail, and chooses whether the agent run replays or opens settled.', - }, - ], - }, - { - name: 'data-rooms', - title: 'Data Rooms', - description: - 'Secure document rooms for an organization and its sub-organizations: access that carries down the hierarchy, folder shares that expire, agreements before entry, view-only and watermarked documents, Q&A, reading insights, and a room assistant that only cites what the reader can open.', - previewDescription: - 'Open Project Atlas, ask why someone can see a folder, preview the room as a bidder or a share-only advisor, share a folder until a date, accept the agreement as a guest, answer a question from a drafted reply, and follow who read what in Insights.', - previewHeight: 820, - whenToUse: [ - 'Use Data Rooms when documents must be shared with people inside and outside an organization under rules people can see and explain: deal rooms, board packs, fundraising, audits, client portals.', - 'Use the Storage Browser block for plain file management without rooms, roles, agreements, or guests.', - ], - usage: { - description: - 'Pass the workspace as one data object and handle every change through onCommand. The template owns navigation, dialogs, previews, and optimistic updates; the host owns storage, delivery, authorization, and the activity log.', - example: `'use client'; - -import { useRouter } from 'next/navigation'; - -import { - DataRooms, - type DataRoomsAction, - type DataRoomsCommand, - type DataRoomsData -} from '@/components/ui/data-rooms'; - -type DealRoomsProps = Readonly<{ - data: DataRoomsData; - save: (command: DataRoomsCommand) => Promise; - download: (documentId: string) => void; -}>; - -export function DealRooms({ data, save, download }: DealRoomsProps) { - const router = useRouter(); - - const handleAction = (action: DataRoomsAction) => { - if (action.type === 'download') download(action.documentId); - if (action.type === 'org-menu' && action.item === 'log-out') router.push('/logout'); - }; - - return ( -
- { - await save(command); - router.refresh(); - }} - onAction={handleAction} - /> -
- ); -}`, - }, - state: { - title: 'Access, previews, and changes', - description: - 'Access is worked out from data by the exported effectiveAccess helper: owners and admins carry down through sub-units into rooms, members only when inheritance allows and the room is not restricted, folder shares cover their subtree until they expire, read-only members lose every write, and view-only documents lose downloads. It drives which controls appear, the “Why can they?” trace, and Preview as, and your server still decides every request. Commands apply locally after onCommand resolves, and new data from the host replaces them.', - }, - composition: [ - 'Built on the workspace kit (shell, sidebar, panels, tables, canvas) and the AI kit (prompt input, chat container, suggestions), so it matches Agents Builder and Billing.', - 'Every view, tab, sheet, and dialog is exported, along with effectiveAccess, describeGrant, applyDataRoomsCommand, and answerFromScript for hosts that build their own shell or run the same rules on the server.', - 'Pass renderPreview to draw real pages in the viewer and onAskAssistant to answer from your own runtime; without them the template draws placeholder pages and answers from data.assistant.', - ], - accessibility: [ - 'Room sections are a tab list with arrow-key navigation, and every icon-only control has a label and a tooltip.', - 'The access map is a focusable canvas: arrow keys pan, plus and minus zoom, and zero resets to 100%.', - 'Permissions are written out as labelled chips, never conveyed by colour alone, and watermarks are announced as text.', - 'Assistant answers appear instantly when reduced motion is requested.', - ], - api: [ - { - name: 'data', - type: 'DataRoomsData', - behavior: - 'The organization and its units, people, roles, memberships, inheritance rules, rooms with their protections and agreement, documents and folders, folder shares, invites, acceptances, activity, questions, and assistant script. DATA_ROOMS_DEMO is a complete example.', - }, - { - name: 'onCommand', - type: '(command: DataRoomsCommand) => Promise | void', - behavior: - 'Every change: create and update rooms, invite, change roles, share and revoke folders, upload, move, accept the agreement, ask and answer questions, edit roles and inheritance. Reject with an Error to keep the dialog open with its message.', - }, - { - name: 'onAction', - type: '(action: DataRoomsAction) => void', - behavior: 'Downloads, document views for your access log, activity export, search, contact, and organization menu items.', - }, - { - name: 'view / roomId / tab', - type: "'home' | 'access-map' | 'activity' | 'room'", - behavior: 'Each has a default and an onChange, so a router can control where the workspace is.', - }, - { - name: 'previewAs / defaultPreviewAs', - type: 'string | null', - behavior: 'Shows the workspace as another person sees it. Changes are disabled while previewing.', - }, - { - name: 'onAskAssistant', - type: '(request) => Promise', - behavior: 'Answers the room assistant from your runtime. Return only citations the asker may open.', - }, - { - name: 'renderPreview', - type: '(document, context) => ReactNode', - behavior: 'Draws document pages in the viewer, given the reader and whether to watermark and block downloads.', - }, - { - name: 'views / locale / timeZone', - type: 'DataRoomsView[] / string', - behavior: 'Limits the views to what your data can back, and sets date and number formatting (en-US in UTC by default).', - }, - ], - }, - { - name: 'billing-account', - href: '/blocks/billing/account', - section: 'billing', - title: 'Billing Account', - description: - 'The customer side of Constructive billing: an account switcher and six views over one account’s plan, usage by credit pool, credits, invoices, and ledger. Works for Constructive’s own platform billing and for any tenant app billing its customers.', - previewDescription: - 'Switch scenarios (overdue, suspended, checkout pending, scheduled downgrade, free, read-only member, tenant app), open a meter to see its daily usage and waterfall, preview a downgrade with its conflicts, buy a pack, redeem a gift code (try HACKWEEK-2026 or TEAM-SEATS), and filter the ledger.', - previewHeight: 820, - whenToUse: [ - 'Use Billing Account as the billing destination for a signed-in person or organization: what plan they are on, what they have used, what is left, and what they paid.', - 'Use the Billing Console template for the operator side (catalog, customers, provider, standing), and billing-kit leaves when one card or table belongs inside an existing page.', - ], - usage: { - description: - 'Map the account’s billing rows into BillingAccountData and hand purchases to your provider through callbacks. The template owns navigation, previews, and optimistic local state; the host owns checkout, the customer portal, and persistence.', - example: `'use client'; - -import { useRouter } from 'next/navigation'; - -import { - BillingAccount, - type BillingAccountAction, - type BillingAccountData, - type PlanChangeRequest, - type RedeemResult -} from '@/components/ui/billing-account'; - -type AccountBillingProps = Readonly<{ - data: BillingAccountData; - now: string; - changePlan: (request: PlanChangeRequest) => Promise<{ checkoutUrl?: string }>; - checkoutPack: (slug: string) => Promise; - redeem: (code: string) => Promise; - openPortal: () => Promise; - /** From the \`?code=\` of a promo link, if any. */ - promoCode?: string; -}>; - -export function AccountBilling({ data, now, changePlan, checkoutPack, redeem, openPortal, promoCode }: AccountBillingProps) { - const router = useRouter(); - - const handleAction = async (action: BillingAccountAction) => { - if (action.type === 'open-portal') window.location.assign(await openPortal()); - if (action.type === 'switch-account') router.push(\`/billing/\${action.accountId}\`); - }; - - return ( -
- { - const { checkoutUrl } = await changePlan(request); - if (checkoutUrl) window.location.assign(checkoutUrl); - }} - onBuyCredits={async (pack) => window.location.assign(await checkoutPack(pack.slug))} - onRedeemCode={redeem} - initialRedeemCode={promoCode} - onAction={handleAction} - /> -
- ); -}`, - }, - state: { - title: 'Views, previews, and optimistic state', - description: - 'The view is uncontrolled by default; pass view and onViewChange to sync it with a router, and views to hide the ones your data cannot back. After a callback resolves the template shows the result locally (a scheduled change, a pending checkout, a new plan); passing new data resets it to the host’s truth.', - }, - composition: [ - 'Built on workspace-kit (the same shell as Agents Builder: sidebar rail, drawer, view frames, filter pills) and billing-kit, so every card, table, and dialog is reusable on its own.', - 'Overview: BillingStatusBanner (admin hold → suspension → grace → checkout pending → review → scheduled change), CurrentPlanCard, CreditWallet, PoolGrid, LimitList, LedgerTimeline, and FeatureCapList.', - 'Usage: headline StatTiles with a projection to period end, UsageTree (universal → category pools → meters, each with plan marker and credit cost), MeterDetailSheet with daily Sparkline and request windows, LimitList, UsageAlertList, and FeatureCapList.', - 'Plans: IntervalSwitch and PlanComparison, which hides plans with no active price except the account’s current one; choosing a plan opens PlanChangeDialog, which refuses a plan without an active price, which diffs entitlements, flags limits already exceeded, and offers “now” or “at period end” when the provider can schedule changes.', - 'Gift codes: RedeemCodeDialog opens from the overview, the credits view, the account menu, or a ?code= link (initialRedeemCode). The host answers with the redemption or a typed refusal (not found, paused, expired, used up, already redeemed, not eligible); the dialog lists every grant, and the credits, balances, limits, and ledger update right away.', - 'Credits: CreditWallet, RedeemCodeField, CreditPackGrid, CreditGrantList in draw-down order, and RedemptionList of codes this account redeemed. Invoices: payment method panel (PaymentMethodRow from the host’s paymentMethod summary, plus the provider portal), InvoiceTable with expandable lines, AdjustmentList. Activity: LedgerTimeline filtered by class.', - 'Provider-neutral: the active BillingProviderDescriptor decides which actions appear (hosted checkout, customer portal, scheduled changes) and how the provider is named, including merchant-of-record copy. Customers see the provider by name only; object ids and dashboard links stay in the console. Stripe ships as STRIPE_PROVIDER; hosts register others the same way.', - ], - accessibility: [ - 'Status never relies on colour: every badge carries a label, meters expose values through the meter role, and warnings in the sidebar have screen-reader text.', - 'The plan-change preview is a labelled dialog with radio choices for timing and inline error text when the host refuses the change.', - 'The collapsed sidebar keeps labels through tooltips and aria-label; below the sidebar width navigation moves into a drawer.', - 'Entrance motion and bar fills turn off under reduced motion; view swaps never animate the whole page.', - ], - api: [ - { name: 'data', type: 'BillingAccountData', behavior: 'Accounts, plans and prices, comparison rows, meters and balances, the subscription, credit grants and packs, ledger, invoices, limits, caps, alerts, and (platform) database standing. BILLING_ACCOUNT_DEMO and billingAccountScenario() are complete examples.' }, - { name: 'view / defaultView / onViewChange', type: "'overview' | 'usage' | 'plans' | 'credits' | 'invoices' | 'activity'", behavior: 'Controls or seeds the active view.' }, - { name: 'views', type: 'BillingAccountView[]', behavior: 'Limits navigation to the views the host can back; defaults to all six.' }, - { name: 'onChangePlan', type: '(request: PlanChangeRequest) => Promise', behavior: 'Receives the plan, price, timing, and whether a hosted checkout is needed. Reject to keep the preview open with the message.' }, - { name: 'onBuyCredits', type: '(pack) => Promise', behavior: 'Starts a pack checkout.' }, - { name: 'onRedeemCode', type: '(code: string) => Promise', behavior: 'Redeems a normalised gift code for this account. Resolve with { status: \'redeemed\', redemption } (what it granted, each grant\'s expiry resolved) or { status: \'refused\', reason, message? }. Without it, redeeming is hidden.' }, - { name: 'initialRedeemCode', type: 'string', behavior: 'Opens the redeem dialog prefilled, e.g. from a promo link; a new value reopens it.' }, - { name: 'onAlertsChange', type: '(alerts: UsageAlert[]) => void', behavior: 'Persists usage alert edits.' }, - { name: 'onAction', type: '(action: BillingAccountAction) => void', behavior: 'Portal, invoice, pay, cancel scheduled change, contact sales or support, account switch, and account-menu items.' }, - { name: 'locale / timeZone / now', type: 'string', behavior: 'Formatting; pass now from the server so relative dates render identically on both sides.' }, - { name: 'theme / onThemeChange / defaultSidebarCollapsed', type: "'light' | 'dark' | 'system' / boolean", behavior: 'Drives the appearance switch in the account menu and the initial rail state.' }, - ], - buildingBlocks: [ - { name: 'BillingStatusBanner / Notice / LifecycleBadge', type: 'status', behavior: 'The one notice an account needs now, and lifecycle labels for active, grace, suspended, checkout pending, ended, and review required.' }, - { name: 'CurrentPlanCard / PriceTag / IntervalSwitch', type: 'plan', behavior: 'Plan, price, period progress, next invoice, and scheduled change.' }, - { name: 'PlanComparison / PlanChangeDialog', type: 'plan', behavior: 'Side-by-side plans (plans without an active price are hidden) and the preview that diffs entitlements and picks timing.' }, - { name: 'UsageTree / PoolGrid / MeterDetailSheet', type: 'usage', behavior: 'The credit waterfall, busiest pools, and one meter in depth.' }, - { name: 'AllowanceBar / UsageFigure / Sparkline / PeriodTrack', type: 'primitives', behavior: 'Bars with soft thresholds and plan markers, figures, daily bars, and the billing period as a thin segmented bar, one segment per day.' }, - { name: 'CreditWallet / CreditGrantList / CreditPackGrid', type: 'credits', behavior: 'Balance composition, grants in spend order, and packs.' }, - { name: 'RedeemCodeDialog / RedeemCodeField / RedemptionList / CodeGrantList', type: 'gift codes', behavior: 'Redeem a code, see exactly what it added (e.g. +25,000 compute credits for 30 days), typed refusals, and redeemed-code history.' }, - { name: 'LimitList / FeatureCapList / UsageAlertList', type: 'entitlements', behavior: 'Counted limits, plan features, and usage alerts.' }, - { name: 'InvoiceTable / AdjustmentList / LedgerTimeline', type: 'history', behavior: 'Invoices with lines, refunds and disputes, and the ledger by day.' }, - ], - }, - { - name: 'billing-console', - href: '/blocks/billing/console', - section: 'billing', - title: 'Billing Console', - description: - 'The operator side of Constructive billing, for the platform or a tenant database: revenue and attention, the catalog (plans and prices, entitlements, meters, packs and codes), customers, the payment provider with readiness and the billing switch, and platform database standing.', - previewDescription: - 'Switch between the platform console and a tenant setting up billing. Edit the entitlement matrix and save, open a customer and grant credits, run the readiness check, try the locked billing switch, change the credit rate, switch providers, create or bulk-create gift codes and see who redeemed them, and place or lift an admin hold.', - previewHeight: 820, - whenToUse: [ - 'Use Billing Console where operators manage what a billing module sells and to whom: Constructive’s platform team, or a developer running billing for their own app.', - 'Use Billing Account for the customer side of the same module.', - ], - usage: { - description: - 'Map the billing module’s catalog, customers, provider connection, readiness verdict, and settings into BillingConsoleData, and route every write to your API. Provider credentials are write-only: the console only learns whether each one is set.', - example: `'use client'; - -import { - BillingConsole, - type BillingConsoleData -} from '@/components/ui/billing-console'; - -type OperatorBillingProps = Readonly<{ - data: BillingConsoleData; - api: { - saveEntitlements: (changes: { planId: string; kind: string; key: string; value: number }[]) => Promise; - storeProviderSecrets: (providerId: string, values: Record) => Promise; - enqueueReadinessCheck: () => Promise; - setBillingEnabled: (enabled: boolean) => Promise; - grantCredits: (customerId: string, meter: string, amount: number, reason: string) => Promise; - }; -}>; - -export function OperatorBilling({ data, api }: OperatorBillingProps) { - return ( -
- - api.grantCredits(request.customerId, request.meterSlug, request.amount, request.reason) - } - /> -
- ); -}`, - }, - state: { - title: 'Drafts, gates, and optimistic state', - description: - 'Entitlement edits stay a local draft with a save bar until onSaveEntitlements resolves. The billing switch refuses to turn on without a passing readiness verdict under 24 hours old, mirroring the database gate; turning it off is always allowed. Toggles, grants, holds, and provider connections show locally after the host resolves and reset with new data.', - }, - composition: [ - 'Same shell as Billing Account and Agents Builder (workspace-kit), with a workspace menu that marks the scope as Platform or Tenant and a footer showing whether billing is live.', - 'Overview: StatTiles for recurring revenue, customers, and revenue at risk; a “Needs attention” list (readiness, overdue customers, reviews, catalog sync, usage-sync failures, suspended databases); revenue bars; customers by plan.', - 'Catalog: PlanPriceTable (immutable prices, sync state, provider ids), EntitlementMatrix (plans × limits, meter allowances, caps, editable with ∞), MeterCatalogTable grouped by pool, and CreditPackTable (flags packs priced below the credit rate).', - 'Gift codes: CreditCodeTable with live, paused, expired, and used-up codes; CreditCodeDialog to create or edit one (generated or typed code, grants on meters such as the compute pool or on limits such as seats, redemption cap, expiry, pause); BulkCodeDialog for single-use batches with copy and CSV download; CreditCodeSheet with every redemption.', - 'Customers: filters and search over CustomerTable; CustomerDetailSheet with balances, credits and GrantCreditsForm, overrides, request windows, the provider operations log, and invoices.', - 'Provider: the active provider card with write-only credential state, ReadinessChecklist with “Check now”, the gated billing switch, the credit rate, usage sync, and ProviderConnectDialog for connecting or switching (one active provider at a time).', - 'Standing (platform only): StandingTable with billing suspensions and admin holds, a reasoned hold dialog, and release.', - ], - accessibility: [ - 'Tables use real table semantics with scoped headers; switches have descriptive labels such as “Retire the monthly Pro price”.', - 'Matrix cells are labelled inputs (“Databases for Team”) that commit on blur or Enter and cancel on Escape; the unlimited toggle is a pressed-state button.', - 'The readiness checklist announces its running state, and every check keeps its text status.', - 'Credential inputs never echo stored values; errors are tied to fields with aria-describedby.', - ], - api: [ - { name: 'data', type: 'BillingConsoleData', behavior: 'Scope, workspace, providers and the active connection, readiness verdict, settings, usage sync, plans, entitlement groups, meters, packs, codes, customers, standing, and a revenue trend. BILLING_CONSOLE_DEMO and BILLING_CONSOLE_TENANT_DEMO are complete examples.' }, - { name: 'view / defaultView / onViewChange / views', type: "'overview' | 'catalog' | 'customers' | 'provider' | 'standing'", behavior: 'Controls or seeds the active view and limits navigation; standing only appears at platform scope.' }, - { name: 'onSaveEntitlements', type: '(changes: EntitlementChange[]) => Promise', behavior: 'Persists the matrix draft; reject to keep it unsaved with the message.' }, - { name: 'onConnectProvider', type: '(providerId, values) => Promise', behavior: 'Stores credentials and makes the provider active.' }, - { name: 'onRunReadinessCheck', type: '() => Promise', behavior: 'Enqueues the readiness job; resolve with the new verdict to show it.' }, - { name: 'onToggleBilling / onCreditRateChange', type: '(enabled) / (creditsPerCent) => Promise', behavior: 'Flip enable_billing and set credits_per_cent; reject with the gate’s error to show it.' }, - { name: 'onGrantCredits', type: '(request: GrantCreditsRequest) => Promise', behavior: 'Grants credits to one customer with a type, optional expiry, and a ledger reason.' }, - { name: 'onSaveCode / onCreateCodes', type: '(draft, existing?) => Promise / (drafts) => Promise', behavior: 'Create or edit one gift code, or a bulk batch of single-use codes. Without them the code actions are hidden.' }, - { name: 'onHoldDatabase / onReleaseDatabase', type: '(database, note?) => Promise', behavior: 'Platform scope: place an admin hold or lift a suspension.' }, - { name: 'onAction', type: '(action: BillingConsoleAction) => void', behavior: 'Plan, price, meter, pack, and code toggles; new plan, price, and pack; override removal; scheduled-change cancellation; workspace menu.' }, - ], - buildingBlocks: [ - { name: 'EntitlementMatrix / PlanPriceTable', type: 'catalog', behavior: 'Plans × entitlements with in-place editing, and plans with their immutable prices and provider mirror.' }, - { name: 'MeterCatalogTable / CreditPackTable', type: 'catalog', behavior: 'Meters grouped by pool, and packs checked against the credit rate.' }, - { name: 'CreditCodeTable / CreditCodeDialog / BulkCodeDialog / CreditCodeSheet / CodeItemsEditor', type: 'gift codes', behavior: 'List, create, bulk-create, pause, and inspect gift codes and their redemptions.' }, - { name: 'CustomerTable / CustomerDetailSheet / GrantCreditsForm', type: 'customers', behavior: 'Subscribers, one customer in depth, and credit grants.' }, - { name: 'ProviderConnectDialog / ProviderCredentialForm / ProviderCard / ProviderMark', type: 'provider', behavior: 'Pick or switch providers and store write-only credentials, driven by BillingProviderDescriptor.' }, - { name: 'ReadinessChecklist / SyncBadge / ExternalRef', type: 'provider', behavior: 'Doctor checks with provider copy, catalog sync state, and provider ids with copy and deep links.' }, - { name: 'StandingTable', type: 'platform', behavior: 'Billing suspensions and admin holds with hold and release.' }, - ], - }, -]; - -const APPLICATION_BLOCK_BY_NAME = new Map( - APPLICATION_BLOCKS.map((block) => [block.name, block] as const), -); - -export function getApplicationBlock(name: string): ApplicationBlockDoc | undefined { - return APPLICATION_BLOCK_BY_NAME.get(name as ApplicationBlockDoc['name']); -} - -/** The docs route for an application block. */ -export function applicationBlockHref(block: Pick) { - return block.href ?? `/blocks/${block.name}`; -} - -const APPLICATION_BLOCK_BY_HREF = new Map( - APPLICATION_BLOCKS.map((block) => [applicationBlockHref(block), block] as const), -); - -/** The application block documented at a (normalised) path, if any. */ -export function getApplicationBlockByPath(path: string): ApplicationBlockDoc | undefined { - return APPLICATION_BLOCK_BY_HREF.get(path); -} diff --git a/apps/blocks/src/lib/application-blocks/agents-builder.ts b/apps/blocks/src/lib/application-blocks/agents-builder.ts new file mode 100644 index 00000000..3adfe840 --- /dev/null +++ b/apps/blocks/src/lib/application-blocks/agents-builder.ts @@ -0,0 +1,122 @@ +import type { ApplicationBlockDoc } from './types'; + +export const agentsBuilderBlock: ApplicationBlockDoc = { + name: 'agents-builder', + title: 'Agents Builder', + description: + 'A complete agent workspace template: chat with inline questions and tool traces, an integrations directory with a two-step connect flow, a skills library, and an agent detail view with a live run panel and pannable canvas.', + previewDescription: + 'Start a recommended chat, answer the agent’s questions, connect a source, triage agent-drafted replies in the Inbox, pause or run schedules, then open Revenue Analyst to watch a run reason, query the warehouse, fan out across sub-agents and skills, and wait for your approval.', + previewHeight: 820, + whenToUse: [ + 'Use Agents Builder as the starting shell for a product where people chat with agents, connect the apps those agents read from, and inspect what each agent runs.', + 'Install the AI kit primitives on their own when you only need a chat surface or a single agent card inside an existing layout.', + ], + usage: { + description: + 'Pass workspace data and handle side effects through callbacks. The template owns navigation between its views, the connect dialog, and scripted playback; the host owns routing, OAuth, persistence, and model calls.', + example: `'use client'; + +import { useRouter } from 'next/navigation'; + +import { + AgentsBuilder, + type AgentsBuilderAction, + type AgentsBuilderData, + type Integration +} from '@/components/ui/agents-builder'; + +type AgentWorkspaceProps = Readonly<{ + data: AgentsBuilderData; + startOAuth: (integration: Integration) => Promise; + saveConnection: ( + integrationId: string, + toolIds: string[] + ) => Promise; +}>; + +export function AgentWorkspace({ + data, + startOAuth, + saveConnection +}: AgentWorkspaceProps) { + const router = useRouter(); + + const handleAction = (action: AgentsBuilderAction) => { + if (action.type === 'navigate') router.push(\`/\${action.target}\`); + if (action.type === 'open-agent') router.push(\`/agents/\${action.agentId}\`); + }; + + return ( +
+ + saveConnection(integration.id, toolIds) + } + onAction={handleAction} + /> +
+ ); +}`, + }, + state: { + title: 'Views, connections, and playback', + description: + 'The active view is uncontrolled by default; pass view and onViewChange to sync it with a router. Connections start from each integration’s connected flag and update only after onConnectIntegration resolves. Chat replies and the agent run play from scripted beats, so map live model and run events onto the same shapes when you connect a runtime.', + }, + composition: [ + 'Built from the AI kit: PromptInput, ChatContainer, ToolTrace, ThinkingStatus, AskCard, ConnectPrompt, AgentDraftCard, and UsageNotice, so each surface can be reused outside the template.', + 'Each view (ChatView, IntegrationsView, SkillsView, AgentView) and the ConnectIntegrationDialog is exported for hosts that want their own shell or router.', + 'Integration marks fall back to tinted monograms; pass real brand marks through each integration’s mark field.', + ], + accessibility: [ + 'Every icon-only control has a label and a tooltip; the collapsed sidebar keeps labels available through tooltips and aria-label.', + 'The agent canvas is a focusable region: arrow keys pan, plus and minus zoom, and zero resets to 100%.', + 'Clarifying questions use radio semantics with lettered options and a labeled free-text row, and streamed status lines are announced politely.', + 'Scripted playback shortens to near-instant steps when reduced motion is requested.', + ], + api: [ + { + name: 'data', + type: 'AgentsBuilderData', + behavior: + 'Workspace name, models, integrations and categories, agents, skills, starter packs, chat recommendations and replies, the agent blueprint, and its run script. AGENTS_BUILDER_DEMO is a complete example.', + }, + { + name: 'view / defaultView / onViewChange', + type: "'chat' | 'integrations' | 'skills' | 'agent'", + behavior: 'Controls or seeds the active view.', + }, + { + name: 'onAuthorizeIntegration', + type: '(integration) => Promise', + behavior: + 'Runs the provider consent handoff. Resolve to advance to tool selection; reject to return to sign-in.', + }, + { + name: 'onConnectIntegration', + type: '(integration, toolIds) => void | Promise', + behavior: + 'Persists a connection with the tools left enabled. Rejecting keeps the dialog open on the tools step.', + }, + { + name: 'onAction', + type: '(action: AgentsBuilderAction) => void', + behavior: + 'Receives every host-owned control: navigation, search, sharing, skill and agent links, workspace menu items, stop, and follow-ups.', + }, + { + name: 'theme / onThemeChange', + type: "'light' | 'dark' | 'system'", + behavior: 'Drives the appearance switch in the workspace menu.', + }, + { + name: 'defaultSidebarCollapsed / autoplayRun', + type: 'boolean', + behavior: + 'Starts with the icon rail, and chooses whether the agent run replays or opens settled.', + }, + ], +}; diff --git a/apps/blocks/src/lib/application-blocks/billing-account.ts b/apps/blocks/src/lib/application-blocks/billing-account.ts new file mode 100644 index 00000000..5e201ea2 --- /dev/null +++ b/apps/blocks/src/lib/application-blocks/billing-account.ts @@ -0,0 +1,117 @@ +import type { ApplicationBlockDoc } from './types'; + +export const billingAccountBlock: ApplicationBlockDoc = { + name: 'billing-account', + href: '/blocks/billing/account', + section: 'billing', + title: 'Billing Account', + description: + 'The customer side of Constructive billing: an account switcher and six views over one account’s plan, usage by credit pool, credits, invoices, and ledger. Works for Constructive’s own platform billing and for any tenant app billing its customers.', + previewDescription: + 'Switch scenarios (overdue, suspended, checkout pending, scheduled downgrade, free, read-only member, tenant app), open a meter to see its daily usage and waterfall, preview a downgrade with its conflicts, buy a pack, redeem a gift code (try HACKWEEK-2026 or TEAM-SEATS), and filter the ledger.', + previewHeight: 820, + whenToUse: [ + 'Use Billing Account as the billing destination for a signed-in person or organization: what plan they are on, what they have used, what is left, and what they paid.', + 'Use the Billing Console template for the operator side (catalog, customers, provider, standing), and billing-kit leaves when one card or table belongs inside an existing page.', + ], + usage: { + description: + 'Map the account’s billing rows into BillingAccountData and hand purchases to your provider through callbacks. The template owns navigation, previews, and optimistic local state; the host owns checkout, the customer portal, and persistence.', + example: `'use client'; + +import { useRouter } from 'next/navigation'; + +import { + BillingAccount, + type BillingAccountAction, + type BillingAccountData, + type PlanChangeRequest, + type RedeemResult +} from '@/components/ui/billing-account'; + +type AccountBillingProps = Readonly<{ + data: BillingAccountData; + now: string; + changePlan: (request: PlanChangeRequest) => Promise<{ checkoutUrl?: string }>; + checkoutPack: (slug: string) => Promise; + redeem: (code: string) => Promise; + openPortal: () => Promise; + /** From the \`?code=\` of a promo link, if any. */ + promoCode?: string; +}>; + +export function AccountBilling({ data, now, changePlan, checkoutPack, redeem, openPortal, promoCode }: AccountBillingProps) { + const router = useRouter(); + + const handleAction = async (action: BillingAccountAction) => { + if (action.type === 'open-portal') window.location.assign(await openPortal()); + if (action.type === 'switch-account') router.push(\`/billing/\${action.accountId}\`); + }; + + return ( +
+ { + const { checkoutUrl } = await changePlan(request); + if (checkoutUrl) window.location.assign(checkoutUrl); + }} + onBuyCredits={async (pack) => window.location.assign(await checkoutPack(pack.slug))} + onRedeemCode={redeem} + initialRedeemCode={promoCode} + onAction={handleAction} + /> +
+ ); +}`, + }, + state: { + title: 'Views, previews, and optimistic state', + description: + 'The view is uncontrolled by default; pass view and onViewChange to sync it with a router, and views to hide the ones your data cannot back. After a callback resolves the template shows the result locally (a scheduled change, a pending checkout, a new plan); passing new data resets it to the host’s truth.', + }, + composition: [ + 'Built on workspace-kit (the same shell as Agents Builder: sidebar rail, drawer, view frames, filter pills) and billing-kit, so every card, table, and dialog is reusable on its own.', + 'Overview: BillingStatusBanner (admin hold → suspension → grace → checkout pending → review → scheduled change), CurrentPlanCard, CreditWallet, PoolGrid, LimitList, LedgerTimeline, and FeatureCapList.', + 'Usage: headline StatTiles with a projection to period end, UsageTree (universal → category pools → meters, each with plan marker and credit cost), MeterDetailSheet with daily Sparkline and request windows, LimitList, UsageAlertList, and FeatureCapList.', + 'Plans: IntervalSwitch and PlanComparison, which hides plans with no active price except the account’s current one; choosing a plan opens PlanChangeDialog, which refuses a plan without an active price, which diffs entitlements, flags limits already exceeded, and offers “now” or “at period end” when the provider can schedule changes.', + 'Gift codes: RedeemCodeDialog opens from the overview, the credits view, the account menu, or a ?code= link (initialRedeemCode). The host answers with the redemption or a typed refusal (not found, paused, expired, used up, already redeemed, not eligible); the dialog lists every grant, and the credits, balances, limits, and ledger update right away.', + 'Credits: CreditWallet, RedeemCodeField, CreditPackGrid, CreditGrantList in draw-down order, and RedemptionList of codes this account redeemed. Invoices: payment method panel (PaymentMethodRow from the host’s paymentMethod summary, plus the provider portal), InvoiceTable with expandable lines, AdjustmentList. Activity: LedgerTimeline filtered by class.', + 'Provider-neutral: the active BillingProviderDescriptor decides which actions appear (hosted checkout, customer portal, scheduled changes) and how the provider is named, including merchant-of-record copy. Customers see the provider by name only; object ids and dashboard links stay in the console. Stripe ships as STRIPE_PROVIDER; hosts register others the same way.', + ], + accessibility: [ + 'Status never relies on colour: every badge carries a label, meters expose values through the meter role, and warnings in the sidebar have screen-reader text.', + 'The plan-change preview is a labelled dialog with radio choices for timing and inline error text when the host refuses the change.', + 'The collapsed sidebar keeps labels through tooltips and aria-label; below the sidebar width navigation moves into a drawer.', + 'Entrance motion and bar fills turn off under reduced motion; view swaps never animate the whole page.', + ], + api: [ + { name: 'data', type: 'BillingAccountData', behavior: 'Accounts, plans and prices, comparison rows, meters and balances, the subscription, credit grants and packs, ledger, invoices, limits, caps, alerts, and (platform) database standing. BILLING_ACCOUNT_DEMO and billingAccountScenario() are complete examples.' }, + { name: 'view / defaultView / onViewChange', type: "'overview' | 'usage' | 'plans' | 'credits' | 'invoices' | 'activity'", behavior: 'Controls or seeds the active view.' }, + { name: 'views', type: 'BillingAccountView[]', behavior: 'Limits navigation to the views the host can back; defaults to all six.' }, + { name: 'onChangePlan', type: '(request: PlanChangeRequest) => Promise', behavior: 'Receives the plan, price, timing, and whether a hosted checkout is needed. Reject to keep the preview open with the message.' }, + { name: 'onBuyCredits', type: '(pack) => Promise', behavior: 'Starts a pack checkout.' }, + { name: 'onRedeemCode', type: '(code: string) => Promise', behavior: 'Redeems a normalised gift code for this account. Resolve with { status: \'redeemed\', redemption } (what it granted, each grant\'s expiry resolved) or { status: \'refused\', reason, message? }. Without it, redeeming is hidden.' }, + { name: 'initialRedeemCode', type: 'string', behavior: 'Opens the redeem dialog prefilled, e.g. from a promo link; a new value reopens it.' }, + { name: 'onAlertsChange', type: '(alerts: UsageAlert[]) => void', behavior: 'Persists usage alert edits.' }, + { name: 'onAction', type: '(action: BillingAccountAction) => void', behavior: 'Portal, invoice, pay, cancel scheduled change, contact sales or support, account switch, and account-menu items.' }, + { name: 'locale / timeZone / now', type: 'string', behavior: 'Formatting; pass now from the server so relative dates render identically on both sides.' }, + { name: 'theme / onThemeChange / defaultSidebarCollapsed', type: "'light' | 'dark' | 'system' / boolean", behavior: 'Drives the appearance switch in the account menu and the initial rail state.' }, + ], + buildingBlocks: { + description: + 'Each of these installs with billing-kit and works outside the template: pass data and callbacks, and wrap them in BillingFormatProvider for locale, time zone, and clock.', + components: [ + { name: 'BillingStatusBanner / Notice / LifecycleBadge', type: 'status', behavior: 'The one notice an account needs now, and lifecycle labels for active, grace, suspended, checkout pending, ended, and review required.' }, + { name: 'CurrentPlanCard / PriceTag / IntervalSwitch', type: 'plan', behavior: 'Plan, price, period progress, next invoice, and scheduled change.' }, + { name: 'PlanComparison / PlanChangeDialog', type: 'plan', behavior: 'Side-by-side plans (plans without an active price are hidden) and the preview that diffs entitlements and picks timing.' }, + { name: 'UsageTree / PoolGrid / MeterDetailSheet', type: 'usage', behavior: 'The credit waterfall, busiest pools, and one meter in depth.' }, + { name: 'AllowanceBar / UsageFigure / Sparkline / PeriodTrack', type: 'primitives', behavior: 'Bars with soft thresholds and plan markers, figures, daily bars, and the billing period as a thin segmented bar, one segment per day.' }, + { name: 'CreditWallet / CreditGrantList / CreditPackGrid', type: 'credits', behavior: 'Balance composition, grants in spend order, and packs.' }, + { name: 'RedeemCodeDialog / RedeemCodeField / RedemptionList / CodeGrantList', type: 'gift codes', behavior: 'Redeem a code, see exactly what it added (e.g. +25,000 compute credits for 30 days), typed refusals, and redeemed-code history.' }, + { name: 'LimitList / FeatureCapList / UsageAlertList', type: 'entitlements', behavior: 'Counted limits, plan features, and usage alerts.' }, + { name: 'InvoiceTable / AdjustmentList / LedgerTimeline', type: 'history', behavior: 'Invoices with lines, refunds and disputes, and the ledger by day.' }, + ], + }, +}; diff --git a/apps/blocks/src/lib/application-blocks/billing-console.ts b/apps/blocks/src/lib/application-blocks/billing-console.ts new file mode 100644 index 00000000..a7ab39c8 --- /dev/null +++ b/apps/blocks/src/lib/application-blocks/billing-console.ts @@ -0,0 +1,100 @@ +import type { ApplicationBlockDoc } from './types'; + +export const billingConsoleBlock: ApplicationBlockDoc = { + name: 'billing-console', + href: '/blocks/billing/console', + section: 'billing', + title: 'Billing Console', + description: + 'The operator side of Constructive billing, for the platform or a tenant database: revenue and attention, the catalog (plans and prices, entitlements, meters, packs and codes), customers, the payment provider with readiness and the billing switch, and platform database standing.', + previewDescription: + 'Switch between the platform console and a tenant setting up billing. Edit the entitlement matrix and save, open a customer and grant credits, run the readiness check, try the locked billing switch, change the credit rate, switch providers, create or bulk-create gift codes and see who redeemed them, and place or lift an admin hold.', + previewHeight: 820, + whenToUse: [ + 'Use Billing Console where operators manage what a billing module sells and to whom: Constructive’s platform team, or a developer running billing for their own app.', + 'Use Billing Account for the customer side of the same module.', + ], + usage: { + description: + 'Map the billing module’s catalog, customers, provider connection, readiness verdict, and settings into BillingConsoleData, and route every write to your API. Provider credentials are write-only: the console only learns whether each one is set.', + example: `'use client'; + +import { + BillingConsole, + type BillingConsoleData +} from '@/components/ui/billing-console'; + +type OperatorBillingProps = Readonly<{ + data: BillingConsoleData; + api: { + saveEntitlements: (changes: { planId: string; kind: string; key: string; value: number }[]) => Promise; + storeProviderSecrets: (providerId: string, values: Record) => Promise; + enqueueReadinessCheck: () => Promise; + setBillingEnabled: (enabled: boolean) => Promise; + grantCredits: (customerId: string, meter: string, amount: number, reason: string) => Promise; + }; +}>; + +export function OperatorBilling({ data, api }: OperatorBillingProps) { + return ( +
+ + api.grantCredits(request.customerId, request.meterSlug, request.amount, request.reason) + } + /> +
+ ); +}`, + }, + state: { + title: 'Drafts, gates, and optimistic state', + description: + 'Entitlement edits stay a local draft with a save bar until onSaveEntitlements resolves. The billing switch refuses to turn on without a passing readiness verdict under 24 hours old, mirroring the database gate; turning it off is always allowed. Toggles, grants, holds, and provider connections show locally after the host resolves and reset with new data.', + }, + composition: [ + 'Same shell as Billing Account and Agents Builder (workspace-kit), with a workspace menu that marks the scope as Platform or Tenant and a footer showing whether billing is live.', + 'Overview: StatTiles for recurring revenue, customers, and revenue at risk; a “Needs attention” list (readiness, overdue customers, reviews, catalog sync, usage-sync failures, suspended databases); revenue bars; customers by plan.', + 'Catalog: PlanPriceTable (immutable prices, sync state, provider ids), EntitlementMatrix (plans × limits, meter allowances, caps, editable with ∞), MeterCatalogTable grouped by pool, and CreditPackTable (flags packs priced below the credit rate).', + 'Gift codes: CreditCodeTable with live, paused, expired, and used-up codes; CreditCodeDialog to create or edit one (generated or typed code, grants on meters such as the compute pool or on limits such as seats, redemption cap, expiry, pause); BulkCodeDialog for single-use batches with copy and CSV download; CreditCodeSheet with every redemption.', + 'Customers: filters and search over CustomerTable; CustomerDetailSheet with balances, credits and GrantCreditsForm, overrides, request windows, the provider operations log, and invoices.', + 'Provider: the active provider card with write-only credential state, ReadinessChecklist with “Check now”, the gated billing switch, the credit rate, usage sync, and ProviderConnectDialog for connecting or switching (one active provider at a time).', + 'Standing (platform only): StandingTable with billing suspensions and admin holds, a reasoned hold dialog, and release.', + ], + accessibility: [ + 'Tables use real table semantics with scoped headers; switches have descriptive labels such as “Retire the monthly Pro price”.', + 'Matrix cells are labelled inputs (“Databases for Team”) that commit on blur or Enter and cancel on Escape; the unlimited toggle is a pressed-state button.', + 'The readiness checklist announces its running state, and every check keeps its text status.', + 'Credential inputs never echo stored values; errors are tied to fields with aria-describedby.', + ], + api: [ + { name: 'data', type: 'BillingConsoleData', behavior: 'Scope, workspace, providers and the active connection, readiness verdict, settings, usage sync, plans, entitlement groups, meters, packs, codes, customers, standing, and a revenue trend. BILLING_CONSOLE_DEMO and BILLING_CONSOLE_TENANT_DEMO are complete examples.' }, + { name: 'view / defaultView / onViewChange / views', type: "'overview' | 'catalog' | 'customers' | 'provider' | 'standing'", behavior: 'Controls or seeds the active view and limits navigation; standing only appears at platform scope.' }, + { name: 'onSaveEntitlements', type: '(changes: EntitlementChange[]) => Promise', behavior: 'Persists the matrix draft; reject to keep it unsaved with the message.' }, + { name: 'onConnectProvider', type: '(providerId, values) => Promise', behavior: 'Stores credentials and makes the provider active.' }, + { name: 'onRunReadinessCheck', type: '() => Promise', behavior: 'Enqueues the readiness job; resolve with the new verdict to show it.' }, + { name: 'onToggleBilling / onCreditRateChange', type: '(enabled) / (creditsPerCent) => Promise', behavior: 'Flip enable_billing and set credits_per_cent; reject with the gate’s error to show it.' }, + { name: 'onGrantCredits', type: '(request: GrantCreditsRequest) => Promise', behavior: 'Grants credits to one customer with a type, optional expiry, and a ledger reason.' }, + { name: 'onSaveCode / onCreateCodes', type: '(draft, existing?) => Promise / (drafts) => Promise', behavior: 'Create or edit one gift code, or a bulk batch of single-use codes. Without them the code actions are hidden.' }, + { name: 'onHoldDatabase / onReleaseDatabase', type: '(database, note?) => Promise', behavior: 'Platform scope: place an admin hold or lift a suspension.' }, + { name: 'onAction', type: '(action: BillingConsoleAction) => void', behavior: 'Plan, price, meter, pack, and code toggles; new plan, price, and pack; override removal; scheduled-change cancellation; workspace menu.' }, + ], + buildingBlocks: { + description: + 'Each of these installs with billing-kit and works outside the template: pass data and callbacks, and wrap them in BillingFormatProvider for locale, time zone, and clock.', + components: [ + { name: 'EntitlementMatrix / PlanPriceTable', type: 'catalog', behavior: 'Plans × entitlements with in-place editing, and plans with their immutable prices and provider mirror.' }, + { name: 'MeterCatalogTable / CreditPackTable', type: 'catalog', behavior: 'Meters grouped by pool, and packs checked against the credit rate.' }, + { name: 'CreditCodeTable / CreditCodeDialog / BulkCodeDialog / CreditCodeSheet / CodeItemsEditor', type: 'gift codes', behavior: 'List, create, bulk-create, pause, and inspect gift codes and their redemptions.' }, + { name: 'CustomerTable / CustomerDetailSheet / GrantCreditsForm', type: 'customers', behavior: 'Subscribers, one customer in depth, and credit grants.' }, + { name: 'ProviderConnectDialog / ProviderCredentialForm / ProviderCard / ProviderMark', type: 'provider', behavior: 'Pick or switch providers and store write-only credentials, driven by BillingProviderDescriptor.' }, + { name: 'ReadinessChecklist / SyncBadge / ExternalRef', type: 'provider', behavior: 'Doctor checks with provider copy, catalog sync state, and provider ids with copy and deep links.' }, + { name: 'StandingTable', type: 'platform', behavior: 'Billing suspensions and admin holds with hold and release.' }, + ], + }, +}; diff --git a/apps/blocks/src/lib/application-blocks/coding-agent.ts b/apps/blocks/src/lib/application-blocks/coding-agent.ts new file mode 100644 index 00000000..635dbf7d --- /dev/null +++ b/apps/blocks/src/lib/application-blocks/coding-agent.ts @@ -0,0 +1,252 @@ +import type { ApplicationBlockDoc } from './types'; + +/** The coding agent preview's examples, shared with the preview route so a renamed one can't silently fall back. */ +export type CodingAgentExample = 'tabs' | 'sidebar' | 'sub-agents'; + +export const codingAgentBlock: ApplicationBlockDoc = { + name: 'coding-agent', + title: 'Coding Agent', + description: + 'A coding agent workspace template: sessions in a tab strip or a project sidebar, each with a chat transcript, a resizable side panel for reviewing diffs by worktree with each branch’s pull request and failing or pending checks, reading files, previewing the app, and following the session’s other agents and shared tasks, and a terminal dock. Sub-agents, orchestrator/worker runs, and messages between agents show in the transcript, and a session flags when any of its agents needs an answer.', + previewDescription: + 'Pick an example above the preview. In the tabs layout, watch the first session run, approve its test command, comment on a line of the diff, or answer the question waiting in another tab; the sub-agents example opens a lead agent running three workers in parallel.', + previewHeight: 820, + examples: ([ + { + value: 'tabs', + label: 'Tabs layout', + description: + 'Sessions as a tab strip across the top, each tab carrying its live status, with the chat, the review panel, and the file tree side by side under it.', + }, + { + value: 'sidebar', + label: 'Sidebar layout', + description: + 'The same sessions grouped by project in a collapsible sidebar, for products that keep many sessions around rather than a few open at once.', + }, + { + value: 'sub-agents', + label: 'Sub-agents', + description: + 'A lead agent splits a security pass across three workers in their own worktrees: their cards in the transcript, a worker asking for approval, messages between them, and the Agents and Tasks tabs.', + }, + ] satisfies readonly { value: CodingAgentExample; label: string; description: string }[]), + whenToUse: [ + 'Use Coding Agent as the starting shell for a product where people run several coding sessions at once, review what each one changed, and approve the commands it wants to run.', + 'Use Agents Builder for agents that work across connected apps instead of a code checkout, or install the AI kit primitives on their own when you only need a chat surface.', + ], + usage: { + description: + 'Pass projects, sessions, models, and commands as one data object and handle side effects through onAction. The template owns tabs, panels, the terminal dock, and scripted playback; the host owns the agent runtime, the checkout, shell commands, and persistence.', + example: `'use client'; + +import type { ReactNode } from 'react'; + +import { + CodingAgent, + type CodingAgentAction, + type CodingAgentData +} from '@/components/ui/coding-agent'; + +type AgentWorkspaceProps = Readonly<{ + data: CodingAgentData; + runtime: { + send: (sessionId: string, text: string, attachments: string[]) => void; + stop: (sessionId: string) => void; + decide: (sessionId: string, command: string, allow: boolean) => void; + }; + renderPreview: (url: string) => ReactNode | null; +}>; + +export function AgentWorkspace({ + data, + runtime, + renderPreview +}: AgentWorkspaceProps) { + const handleAction = (action: CodingAgentAction) => { + if (action.type === 'send') { + runtime.send(action.sessionId, action.text, action.attachments); + } + if (action.type === 'stop') runtime.stop(action.sessionId); + if (action.type === 'permission') { + runtime.decide( + action.sessionId, + action.command, + action.decision !== 'deny' + ); + } + }; + + return ( +
+ +
+ ); +}`, + }, + guides: [ + { + id: 'harness', + title: 'Driving sessions from your harness', + description: + 'Scripts are for demos. A real session takes the events your harness reports, either as an events array or a store you push into as they stream. The session’s own agent is the run "root"; every other agent is a run with a parent, and its status, messages, tasks, and approval requests are events too, so one-agent loops, sub-agents, and orchestrator/worker teams all draw from the same log.', + example: `'use client'; + +import { useEffect, useMemo } from 'react'; + +import { + CodingAgent, + createHarnessStore, + type CodingAgentAction, + type CodingAgentData, + type HarnessEvent +} from '@/components/ui/coding-agent'; + +type HarnessClient = { + /** Calls back with each event your harness reports for a session; returns an unsubscribe. */ + subscribe: (sessionId: string, onEvent: (event: HarnessEvent) => void) => () => void; + /** Stop, pause, resume, steer, retry, approve, answer, send. */ + control: (action: CodingAgentAction) => void; +}; + +type HostedWorkspaceProps = Readonly<{ + data: CodingAgentData; + harness: HarnessClient; +}>; + +export function HostedWorkspace({ data, harness }: HostedWorkspaceProps) { + // One store per session. The template re-renders a session as its events arrive. + const stores = useMemo( + () => new Map(data.sessions.map((session) => [session.id, createHarnessStore()])), + [data.sessions] + ); + + useEffect(() => { + const unsubscribes = [...stores].map(([sessionId, store]) => + harness.subscribe(sessionId, (event) => store.push(event)) + ); + return () => unsubscribes.forEach((unsubscribe) => unsubscribe()); + }, [harness, stores]); + + return ( +
+ ({ + ...session, + store: stores.get(session.id) + })) + }} + // In host mode controls change nothing locally: your harness acts, + // then reports what happened as events. + onAction={harness.control} + /> +
+ ); +} + +// A lead agent starting a worker in its own worktree, which then asks to run a command. +// The session's own agent is always the run "root". +export const exampleEvents: HarnessEvent[] = [ + { id: 'e1', runId: 'root', kind: 'beat', beat: { kind: 'user', text: 'Check every form posts a CSRF token' } }, + { + id: 'e2', + runId: 'csrf', + kind: 'run.spawned', + run: { + id: 'csrf', + name: 'CSRF', + role: 'worker', + parentRunId: 'root', + task: 'Audit the forms under src/app', + workspace: { kind: 'worktree', branch: 'audit/csrf' } + } + }, + { id: 'e3', runId: 'csrf', kind: 'run.status', status: 'running' }, + { + id: 'e4', + runId: 'csrf', + kind: 'request', + request: { id: 'r1', type: 'approval', title: 'Run the form tests', command: 'pnpm test forms' } + }, + { id: 'e5', runId: 'csrf', kind: 'run.status', status: 'waiting', waitingOn: { kind: 'human', requestId: 'r1' } }, + { id: 'e6', runId: 'csrf', kind: 'message', from: 'csrf', to: ['root'], text: 'Two forms skip the token.', protocol: 'result' } +];`, + }, + ], + state: { + title: 'Sessions, panels, and playback', + description: + 'The layout is uncontrolled by default; pass layout and onLayoutChange to persist it. Open tabs, the active session, recently closed sessions, and each session’s side panel and terminal are local state seeded by defaultActiveSessionId, defaultPanelOpen, and defaultTerminalOpen. Every open session stays mounted, so a run in a background tab keeps playing and marks its tab when it needs an answer, fails, or finishes. A session plays a scripted run, or shows a real harness: pass its events as an \`events\` array, or give it a store from \`createHarnessStore()\` and push events as they arrive. Runs (the session’s own agent and any it starts), their status and usage, messages between agents, handoffs, phases, shared tasks, and approval requests are all events, folded by \`reduceHarness\`; controls such as stop, pause, steer and approve reach the host through onAction. Worktrees are events too: agents with their own worktree register it as they start, \`worktree\`, \`pr\` and \`check\` events report how far each branch is from its base, its pull request and its CI, and changes are kept per worktree, so one agent can work in several and two can change the same file in different branches.', + }, + composition: [ + 'Built on the workspace kit shell, so the sidebar layout collapses to the same icon rail and drawer as the other workspace templates.', + 'The transcript and composer come from the AI kit: Message, Reasoning, ToolGroup, InlineDiff, PlanTracker, AskCard, and ApprovalCard in the timeline, and PromptInput with attachments, a follow-up queue, ModelSelector, ContextRing, and UsageNotice around it.', + 'Diffs and file views render with @pierre/diffs and the file tree with @pierre/trees; both follow the nearest dark-mode class so code surfaces match the host theme.', + 'Session status rings, the new-session mascot, and the playback clock are shared with Agents Builder, so installing Coding Agent also installs that template.', + 'The browser tab draws each URL through renderBrowserPage. Return null to show a can’t-preview state; without a renderer it shows a bundled preview of the demo app.', + ], + accessibility: [ + 'Each session tab’s accessible name includes its live status, such as Working, Waiting for your answer, or New result, and every icon-only control has a label and a tooltip that names its shortcut.', + 'Shortcuts avoid the ones browsers reserve: ⌘K opens the command menu, ⌥⌘N and ⌥⌘W open and close sessions, ⌘P opens a file, ⌘\\ and ⌃` toggle the side panel and terminal, and ⌃1 to ⌃9 jump to a tab. ⌘ is Ctrl outside Apple platforms.', + 'The chat column, side panel, and terminal dock are labeled regions; terminal output is a polite live log, and the terminal input and composer have their own labels.', + 'Line comments on a diff submit with ⌘ or Ctrl+Enter and cancel with Escape, and scripted playback shortens to near-instant steps when reduced motion is requested.', + ], + api: [ + { + name: 'data', + type: 'CodingAgentData', + behavior: + 'Product name, projects with their files, sessions and their scripts, the sessions open on first render, models and the default selection, slash commands, the context window, usage, and the scripts played for follow-ups and new sessions. CODING_AGENT_DEMO is a complete example.', + }, + { + name: 'layout / defaultLayout / onLayoutChange', + type: "'tabs' | 'sidebar'", + behavior: + 'Shows sessions as a tab strip across the top or grouped by project in a sidebar. Switching keeps every session mounted.', + }, + { + name: 'defaultActiveSessionId', + type: 'string', + behavior: 'The session in front on first render; defaults to the first open one.', + }, + { + name: 'defaultPanelOpen / defaultTerminalOpen', + type: 'boolean', + behavior: 'Whether sessions open with the side panel (default true) and the terminal dock (default false) showing.', + }, + { + name: 'renderBrowserPage', + type: '(url: string) => ReactNode | null', + behavior: 'Draws the page for a URL in a browser tab. Return null to show the can’t-preview state.', + }, + { + name: 'onAction', + type: '(action: CodingAgentAction) => void', + behavior: + 'Receives every host-owned effect: new, closed, renamed, deleted, and exported sessions, sent messages, stop, permission decisions, answers, opened files and URLs, line comments, terminal commands, opening or creating a pull request, re-running failed checks, and layout changes.', + }, + { + name: 'framed / className', + type: 'boolean / string', + behavior: 'Sits the workspace in a rounded frame for hosts that show it as an app window, and adds layout classes to the outer shell.', + }, + ], + buildingBlocks: { + description: + 'Each of these comes from the AI kit and installs with the template. They work outside it too: pass them data and callbacks in your own chat or agent surface.', + components: [ + { name: 'PromptInput / PromptInputTray / PromptInputAttachments', type: 'composer', behavior: 'The message box with file and comment chips and the queue of follow-ups typed while the agent works.' }, + { name: 'ModelSelector / ContextRing / UsageNotice', type: 'composer', behavior: 'The model and effort picker, the share of the context window used, and the plan usage warning above the composer.' }, + { name: 'Message / Reasoning / ToolGroup / InlineDiff', type: 'transcript', behavior: 'Replies, collapsible reasoning, batches of read-only tool calls, and edits shown as inline diffs.' }, + { name: 'PlanTracker / AskCard / ApprovalCard', type: 'gates', behavior: 'The agent’s plan, questions that hold the run until answered, and command approvals with allow once, always allow, and deny.' }, + { name: 'Tool / TaskList / TaskRow', type: 'agents', behavior: 'Each sub-agent as a card with its task, live state, last step, and usage, and the shared task board with owners and what each task waits on.' }, + { name: 'ThinkingStatus / SystemMessage / FeedbackBar', type: 'status', behavior: 'The live line naming the next step, a failed run with its retry, and copy controls under the last reply.' }, + ], + }, +}; diff --git a/apps/blocks/src/lib/application-blocks/data-rooms.ts b/apps/blocks/src/lib/application-blocks/data-rooms.ts new file mode 100644 index 00000000..56432c3f --- /dev/null +++ b/apps/blocks/src/lib/application-blocks/data-rooms.ts @@ -0,0 +1,117 @@ +import type { ApplicationBlockDoc } from './types'; + +export const dataRoomsBlock: ApplicationBlockDoc = { + name: 'data-rooms', + title: 'Data Rooms', + description: + 'Secure document rooms for an organization and its sub-organizations: access that carries down the hierarchy, folder shares that expire, agreements before entry, view-only and watermarked documents, Q&A, reading insights, and a room assistant that only cites what the reader can open.', + previewDescription: + 'Open Project Atlas, ask why someone can see a folder, preview the room as a bidder or a share-only advisor, share a folder until a date, accept the agreement as a guest, answer a question from a drafted reply, and follow who read what in Insights.', + previewHeight: 820, + whenToUse: [ + 'Use Data Rooms when documents must be shared with people inside and outside an organization under rules people can see and explain: deal rooms, board packs, fundraising, audits, client portals.', + 'Use the Storage Browser block for plain file management without rooms, roles, agreements, or guests.', + ], + usage: { + description: + 'Pass the workspace as one data object and handle every change through onCommand. The template owns navigation, dialogs, previews, and optimistic updates; the host owns storage, delivery, authorization, and the activity log.', + example: `'use client'; + +import { useRouter } from 'next/navigation'; + +import { + DataRooms, + type DataRoomsAction, + type DataRoomsCommand, + type DataRoomsData +} from '@/components/ui/data-rooms'; + +type DealRoomsProps = Readonly<{ + data: DataRoomsData; + save: (command: DataRoomsCommand) => Promise; + download: (documentId: string) => void; +}>; + +export function DealRooms({ data, save, download }: DealRoomsProps) { + const router = useRouter(); + + const handleAction = (action: DataRoomsAction) => { + if (action.type === 'download') download(action.documentId); + if (action.type === 'org-menu' && action.item === 'log-out') router.push('/logout'); + }; + + return ( +
+ { + await save(command); + router.refresh(); + }} + onAction={handleAction} + /> +
+ ); +}`, + }, + state: { + title: 'Access, previews, and changes', + description: + 'Access is worked out from data by the exported effectiveAccess helper: owners and admins carry down through sub-units into rooms, members only when inheritance allows and the room is not restricted, folder shares cover their subtree until they expire, read-only members lose every write, and view-only documents lose downloads. It drives which controls appear, the “Why can they?” trace, and Preview as, and your server still decides every request. Commands apply locally after onCommand resolves, and new data from the host replaces them.', + }, + composition: [ + 'Built on the workspace kit (shell, sidebar, panels, tables, canvas) and the AI kit (prompt input, chat container, suggestions), so it matches Agents Builder and Billing.', + 'Every view, tab, sheet, and dialog is exported, along with effectiveAccess, describeGrant, applyDataRoomsCommand, and answerFromScript for hosts that build their own shell or run the same rules on the server.', + 'Pass renderPreview to draw real pages in the viewer and onAskAssistant to answer from your own runtime; without them the template draws placeholder pages and answers from data.assistant.', + ], + accessibility: [ + 'Room sections are a tab list with arrow-key navigation, and every icon-only control has a label and a tooltip.', + 'The access map is a focusable canvas: arrow keys pan, plus and minus zoom, and zero resets to 100%.', + 'Permissions are written out as labelled chips, never conveyed by colour alone, and watermarks are announced as text.', + 'Assistant answers appear instantly when reduced motion is requested.', + ], + api: [ + { + name: 'data', + type: 'DataRoomsData', + behavior: + 'The organization and its units, people, roles, memberships, inheritance rules, rooms with their protections and agreement, documents and folders, folder shares, invites, acceptances, activity, questions, and assistant script. DATA_ROOMS_DEMO is a complete example.', + }, + { + name: 'onCommand', + type: '(command: DataRoomsCommand) => Promise | void', + behavior: + 'Every change: create and update rooms, invite, change roles, share and revoke folders, upload, move, accept the agreement, ask and answer questions, edit roles and inheritance. Reject with an Error to keep the dialog open with its message.', + }, + { + name: 'onAction', + type: '(action: DataRoomsAction) => void', + behavior: 'Downloads, document views for your access log, activity export, search, contact, and organization menu items.', + }, + { + name: 'view / roomId / tab', + type: "'home' | 'access-map' | 'activity' | 'room'", + behavior: 'Each has a default and an onChange, so a router can control where the workspace is.', + }, + { + name: 'previewAs / defaultPreviewAs', + type: 'string | null', + behavior: 'Shows the workspace as another person sees it. Changes are disabled while previewing.', + }, + { + name: 'onAskAssistant', + type: '(request) => Promise', + behavior: 'Answers the room assistant from your runtime. Return only citations the asker may open.', + }, + { + name: 'renderPreview', + type: '(document, context) => ReactNode', + behavior: 'Draws document pages in the viewer, given the reader and whether to watermark and block downloads.', + }, + { + name: 'views / locale / timeZone', + type: 'DataRoomsView[] / string', + behavior: 'Limits the views to what your data can back, and sets date and number formatting (en-US in UTC by default).', + }, + ], +}; diff --git a/apps/blocks/src/lib/application-blocks/index.ts b/apps/blocks/src/lib/application-blocks/index.ts new file mode 100644 index 00000000..069e252f --- /dev/null +++ b/apps/blocks/src/lib/application-blocks/index.ts @@ -0,0 +1,43 @@ +import { agentsBuilderBlock } from './agents-builder'; +import { billingAccountBlock } from './billing-account'; +import { billingConsoleBlock } from './billing-console'; +import { codingAgentBlock } from './coding-agent'; +import { dataRoomsBlock } from './data-rooms'; +import { orgChartBlock } from './org-chart'; +import { storageBrowserBlock } from './storage-browser'; +import type { ApplicationBlockDoc } from './types'; + +export type { ApplicationBlockApiRow, ApplicationBlockDoc, ApplicationBlockExample } from './types'; + +/** Every application block's docs, one module each, in the order the docs list them. */ +export const APPLICATION_BLOCKS: readonly ApplicationBlockDoc[] = [ + orgChartBlock, + storageBrowserBlock, + agentsBuilderBlock, + codingAgentBlock, + dataRoomsBlock, + billingAccountBlock, + billingConsoleBlock, +]; + +const APPLICATION_BLOCK_BY_NAME = new Map( + APPLICATION_BLOCKS.map((block) => [block.name, block] as const), +); + +export function getApplicationBlock(name: string): ApplicationBlockDoc | undefined { + return APPLICATION_BLOCK_BY_NAME.get(name as ApplicationBlockDoc['name']); +} + +/** The docs route for an application block. */ +export function applicationBlockHref(block: Pick) { + return block.href ?? `/blocks/${block.name}`; +} + +const APPLICATION_BLOCK_BY_HREF = new Map( + APPLICATION_BLOCKS.map((block) => [applicationBlockHref(block), block] as const), +); + +/** The application block documented at a (normalised) path, if any. */ +export function getApplicationBlockByPath(path: string): ApplicationBlockDoc | undefined { + return APPLICATION_BLOCK_BY_HREF.get(path); +} diff --git a/apps/blocks/src/lib/application-blocks/org-chart.ts b/apps/blocks/src/lib/application-blocks/org-chart.ts new file mode 100644 index 00000000..83edf64e --- /dev/null +++ b/apps/blocks/src/lib/application-blocks/org-chart.ts @@ -0,0 +1,149 @@ +import type { ApplicationBlockDoc } from './types'; + +export const orgChartBlock: ApplicationBlockDoc = { + name: 'org-chart', + title: 'Org Chart', + description: + 'An interactive organization chart on the workspace canvas: automatic top-down layout, pan and zoom, folding teams, a details panel, and drag-to-reassign with a keyboard alternative.', + previewDescription: + 'Select a person to see their manager and reports, fold a team away, or drag a card onto a new manager and watch the preview wire follow.', + previewHeight: 740, + whenToUse: [ + 'Use Org Chart when reporting relationships are central to understanding or managing an organization.', + 'Use a table or tree when dense scanning, bulk editing, or keyboard-only hierarchy management is the primary task.', + ], + usage: { + description: + 'Pass either controlled edges or defaultEdges. The component owns viewport and selection behavior while the host owns persistence and application workflows.', + example: `'use client'; + +import { + OrgChart, + type OrgChartEdge +} from '@/components/ui/org-chart'; + +const reportingLines: OrgChartEdge[] = [ + { + id: 'alex', + parentId: null, + displayName: 'Alex Morgan', + positionTitle: 'Chief Executive Officer', + avatarUrl: null + }, + { + id: 'sam', + parentId: 'alex', + displayName: 'Sam Rivera', + positionTitle: 'VP of Product', + avatarUrl: null + } +]; + +type ReportingLineInput = Readonly<{ + personId: string; + managerId: string; + positionTitle?: string | null; +}>; + +type CompanyOrgChartProps = Readonly<{ + saveReportingLine: ( + reportingLine: ReportingLineInput + ) => void | Promise; + openPositionEditor: (personId: string) => void; + openRemovalConfirmation: (personId: string) => void; +}>; + +export function CompanyOrgChart({ + saveReportingLine, + openPositionEditor, + openRemovalConfirmation +}: CompanyOrgChartProps) { + return ( + + saveReportingLine({ + personId, + managerId, + positionTitle: preserve.positionTitle + }) + } + onEditNode={(person) => openPositionEditor(person.id)} + onRemoveNode={(person) => openRemovalConfirmation(person.id)} + /> + ); +}`, + }, + state: { + title: 'Hierarchy and interaction state', + description: + 'Use edges when the host owns the current hierarchy. Use defaultEdges for optimistic local reparenting, and persist each accepted move through onReparent.', + }, + composition: [ + 'The workspace canvas (shared with Agents Builder) owns pan, pinch, wheel, and keyboard zoom; the block lays flat reporting edges out as a tidy top-down tree and draws bezier wires between cards. More than eight reports without a team of their own stack in columns of up to eight under their manager instead of running in one row.', + 'Selecting a person highlights their reporting chain and opens a details panel with their manager, direct reports, and actions; folded teams show a stacked card edge and the team size.', + 'Node menus delegate editing and removal to the host, so the chart never assumes a router, form system, or destructive-action policy.', + 'Loading and empty states are built in, while success and error messaging remain injectable through observer callbacks.', + ], + accessibility: [ + 'Keep every displayName and positionTitle meaningful because the same labels identify node actions and reporting relationships.', + 'The chart is an ARIA tree: Tab into it, move between people with the arrow keys (inside a stacked column, Up and Down move through the column and Left and Right move between columns), press Enter for details, and use Change manager to reassign without dragging.', + 'Confirm removal in the host workflow before changing data; the node menu reports intent and does not delete records itself.', + ], + api: [ + { + name: 'edges / defaultEdges', + type: 'OrgChartEdge[]', + behavior: + 'Chooses controlled or uncontrolled hierarchy state. Supply exactly one of these properties.', + }, + { + name: 'editable', + type: 'boolean', + behavior: + 'Enables dragging and node actions. Defaults to true.', + }, + { + name: 'isLoading', + type: 'boolean', + behavior: + 'Replaces the chart with a bounded loading surface while hierarchy data is pending.', + }, + { + name: 'onReparent', + type: '(childId, newParentId, preserve) => void | Promise', + behavior: + 'Persists a valid reporting-line change. Uncontrolled mode reverts its optimistic move when the promise rejects.', + }, + { + name: 'onAddToChart / onEditNode / onRemoveNode', + type: 'Callbacks', + behavior: + 'Delegates creation, editing, and removal workflows without coupling the chart to dialogs or routing.', + }, + { + name: 'onReparentSuccess / onReparentError', + type: 'Observer callbacks', + behavior: + 'Lets the host present localized feedback for accepted and rejected reporting-line changes.', + }, + { + name: 'showDetails / onSelectNode', + type: 'boolean / (node | null) => void', + behavior: + 'Shows the built-in details panel for the selected person (default true), and reports selection so a host can render its own panel instead.', + }, + { + name: 'defaultCollapsedIds', + type: 'string[]', + behavior: + 'People whose teams start folded away, useful for large organizations.', + }, + { + name: 'className', + type: 'string', + behavior: + 'Adds layout classes to the chart container; use a height utility to replace the default 600-pixel canvas.', + }, + ], +}; diff --git a/apps/blocks/src/lib/application-blocks/storage-browser.ts b/apps/blocks/src/lib/application-blocks/storage-browser.ts new file mode 100644 index 00000000..48452915 --- /dev/null +++ b/apps/blocks/src/lib/application-blocks/storage-browser.ts @@ -0,0 +1,231 @@ +import type { ApplicationBlockDoc } from './types'; + +export const storageBrowserBlock: ApplicationBlockDoc = { + name: 'storage-browser', + href: '/blocks/storage/browser', + section: 'storage', + title: 'Storage Browser', + description: + 'A controlled storage workspace: buckets in the workspace sidebar, a file list with type-tinted glyphs, search, sort, drag-and-drop upload, a floating selection bar, detail and bucket sheets, and distinct empty states.', + previewDescription: + 'Switch buckets, search and sort files, select a few to see the selection bar, drop files onto the list, or open one for its preview and details.', + previewHeight: 720, + whenToUse: [ + 'Use Storage Browser when people need to inspect and manage objects across several application buckets.', + 'Install a focused storage leaf when the surrounding product already owns navigation, tables, or configuration surfaces.', + ], + usage: { + description: + 'Map GraphQL results into the storage domain types and keep selection, search, sort, folders, and actions in the host. The browser performs no fetching or authorization checks.', + example: `'use client'; + +import { useMemo, useState } from 'react'; + +import { + ObjectDetailSheet, + StorageBrowser, + type ObjectSort, + type StorageBucket, + type StorageObject +} from '@/components/ui/storage'; + +type AssetBrowserActions = Readonly<{ + confirmDelete: (objectIds: string[]) => void; + copyLink: (object: StorageObject) => void; + createBucket: () => void; + download: (object: StorageObject) => void; + rename: (objectId: string, filename?: string) => void; + upload: () => void; +}>; + +function compareObjects( + left: StorageObject, + right: StorageObject, + sort: ObjectSort +) { + let comparison = 0; + if (sort.column === 'filename') { + comparison = (left.filename ?? left.key).localeCompare( + right.filename ?? right.key + ); + } else if (sort.column === 'mimeType') { + comparison = left.mimeType.localeCompare(right.mimeType); + } else if (sort.column === 'size') { + comparison = left.size - right.size; + } else { + comparison = + new Date(left.createdAt).getTime() - + new Date(right.createdAt).getTime(); + } + return sort.direction === 'asc' ? comparison : -comparison; +} + +export function AssetBrowser({ + actions, + buckets, + objects +}: Readonly<{ + actions: AssetBrowserActions; + buckets: StorageBucket[]; + objects: StorageObject[]; +}>) { + const [bucketId, setBucketId] = useState( + buckets[0]?.id ?? null + ); + const [openedObject, setOpenedObject] = useState(null); + const [query, setQuery] = useState(''); + const [selectedIds, setSelectedIds] = useState([]); + const [sort, setSort] = useState({ + column: 'createdAt', + direction: 'desc' + }); + const visibleObjects = useMemo(() => { + const search = query.trim().toLocaleLowerCase(); + return objects + .filter((object) => { + if (object.bucketId !== bucketId) return false; + if (!search) return true; + const searchable = [ + object.filename ?? '', + object.key, + object.mimeType + ].join(' '); + return searchable.toLocaleLowerCase().includes(search); + }) + .sort((left, right) => compareObjects(left, right, sort)); + }, [bucketId, objects, query, sort]); + + return ( + <> + setSelectedIds([])} + onCopyLink={actions.copyLink} + onDelete={(object) => actions.confirmDelete([object.id])} + onDownload={actions.download} + onNewBucket={actions.createBucket} + onOpenObject={setOpenedObject} + onQueryChange={setQuery} + onRename={(object) => actions.rename(object.id)} + onSelectBucket={(nextBucketId) => { + setBucketId(nextBucketId); + setSelectedIds([]); + }} + onSelectionChange={setSelectedIds} + onSortChange={setSort} + onUpload={actions.upload} + /> + { + actions.confirmDelete([objectId]); + setOpenedObject(null); + }} + onDownload={actions.download} + onOpenChange={(open) => { + if (!open) setOpenedObject(null); + }} + onRename={actions.rename} + /> + + ); +}`, + }, + state: { + title: 'Data and action boundaries', + description: + 'Every collection, selection, filter, sort, and mutation callback is controlled. Filter and authorize records before passing them to the block, then refresh its props after a mutation succeeds.', + }, + composition: [ + 'The storage-browser registry item installs the shared storage barrel and every leaf: bucket rail, object table, upload dropzone, detail sheet, bucket configuration sheet, and empty states.', + 'StorageBrowser sits on the shared workspace shell (the same sidebar rail, drawer, and view header as Agents Builder and Billing) and composes the bucket rail, folder path, search, sort menu, object table, selection bar, and empty states; render the installed sheets and upload surface beside it.', + 'The types mirror Constructive storage records, but the UI remains transport-neutral and does not bypass PostgreSQL privileges or RLS.', + ], + accessibility: [ + 'Preserve object filenames, MIME types, sizes, and dates as text so file state never depends on an icon or color alone.', + 'Keep selection controlled and announce the resulting count; bulk deletion should open a confirmation before calling the mutation.', + 'Treat unavailable storage, denied access, no buckets, and an empty bucket as distinct states because they require different recovery actions.', + ], + api: [ + { + name: 'buckets / selectedBucketId / onSelectBucket', + type: 'StorageBucket[] / string | null / callback', + behavior: + 'Supplies the bucket rail and keeps the active bucket controlled by the host.', + }, + { + name: 'objects / selectedIds / onSelectionChange', + type: 'StorageObject[] / string[] / callback', + behavior: + 'Supplies visible objects and controls row and bulk selection.', + }, + { + name: 'sort / onSortChange', + type: 'ObjectSort / callback', + behavior: + 'Reports the requested column and direction; the host returns objects in the corresponding order.', + }, + { + name: 'query / onQueryChange', + type: 'string / callback', + behavior: + 'Controls the search field while filtering or remote querying remains in the host.', + }, + { + name: 'segments / onNavigate', + type: 'StorageBreadcrumbSegment[] / callback', + behavior: + 'Adds optional folder navigation beneath the active bucket header.', + }, + { + name: 'onUpload / onBulkDelete / object callbacks', + type: 'Callbacks', + behavior: + 'Delegates upload, delete, open, download, copy-link, rename, and row-delete workflows.', + }, + { + name: 'onDropFiles', + type: '(files: FileList) => void', + behavior: + 'Turns the file list into a drop target: dragging files over it shows where they will land, and dropping hands the files to the host.', + }, + { + name: 'locale', + type: 'string', + behavior: + 'Formats the counts in the rail, toolbar and selection bar with the reader\'s own separators (for example "de-DE"). Counts render with "en-US" separators by default so the server and browser agree.', + }, + { + name: 'title / defaultSidebarCollapsed', + type: 'string / boolean', + behavior: + 'Names the workspace in the sidebar header (default "Storage") and starts the sidebar as an icon rail.', + }, + { + name: 'isLoading / emptyState / emptyLabel', + type: 'boolean / StorageEmptyStateVariant / string', + behavior: + 'Distinguishes pending results, empty table results, and full-pane storage states.', + }, + { + name: 'bulkDeleteProgress', + type: '{ done: number; total: number; failed: string[] }', + behavior: + 'Shows deterministic progress and partial failures while a host-owned bulk deletion is running.', + }, + { + name: 'className', + type: 'string', + behavior: + 'Adds layout classes to the outer storage workspace.', + }, + ], +}; diff --git a/apps/blocks/src/lib/application-blocks/types.ts b/apps/blocks/src/lib/application-blocks/types.ts new file mode 100644 index 00000000..97d42a16 --- /dev/null +++ b/apps/blocks/src/lib/application-blocks/types.ts @@ -0,0 +1,44 @@ +import type { DocSectionId } from '../doc-sections'; + +export type ApplicationBlockApiRow = Readonly<{ + name: string; + type: string; + behavior: string; +}>; + +/** One way to open a block's live preview, passed to its preview route as `?example=`. */ +export type ApplicationBlockExample = Readonly<{ + value: string; + label: string; + description: string; +}>; + +export type ApplicationBlockDoc = Readonly<{ + name: 'org-chart' | 'storage-browser' | 'agents-builder' | 'coding-agent' | 'data-rooms' | 'billing-account' | 'billing-console'; + /** Docs route when it is not `/blocks/`, e.g. inside a section such as Billing. */ + href?: string; + /** Section hub the block is listed under instead of its own nav item. */ + section?: DocSectionId; + title: string; + description: string; + previewDescription: string; + previewHeight: number; + whenToUse: readonly string[]; + usage: Readonly<{ + description: string; + example: string; + }>; + state: Readonly<{ + title: string; + description: string; + }>; + composition: readonly string[]; + accessibility: readonly string[]; + api: readonly ApplicationBlockApiRow[]; + /** Leaf components the template is built from, each usable on its own, and how they install. */ + buildingBlocks?: Readonly<{ description: string; components: readonly ApplicationBlockApiRow[] }>; + /** Named ways to open the live preview, chosen above the frame; the first is the default. */ + examples?: readonly ApplicationBlockExample[]; + /** Integration guides after basic usage, each with its own code example. */ + guides?: readonly Readonly<{ id: string; title: string; description: string; example: string }>[]; +}>; diff --git a/apps/blocks/src/lib/application-doc-navigation.ts b/apps/blocks/src/lib/application-doc-navigation.ts index 175b2e40..cee43334 100644 --- a/apps/blocks/src/lib/application-doc-navigation.ts +++ b/apps/blocks/src/lib/application-doc-navigation.ts @@ -2,6 +2,7 @@ export type ApplicationDocId = | 'org-chart' | 'storage-browser' | 'agents-builder' + | 'coding-agent' | 'data-rooms' | 'billing-account' | 'billing-console' @@ -21,6 +22,7 @@ export const APPLICATION_DOC_SEQUENCE: readonly ApplicationDocLink[] = [ { id: 'org-chart', title: 'Org Chart' }, { id: 'storage-browser', title: 'Storage Browser', href: '/blocks/storage/browser' }, { id: 'agents-builder', title: 'Agents Builder' }, + { id: 'coding-agent', title: 'Coding Agent' }, { id: 'data-rooms', title: 'Data Rooms' }, { id: 'billing-account', title: 'Billing Account', href: '/blocks/billing/account' }, { id: 'billing-console', title: 'Billing Console', href: '/blocks/billing/console' }, diff --git a/apps/registry/REGISTRY.md b/apps/registry/REGISTRY.md index 122850b7..e18126cd 100644 --- a/apps/registry/REGISTRY.md +++ b/apps/registry/REGISTRY.md @@ -41,6 +41,7 @@ pnpm dlx shadcn@latest add @constructive/workspace-kit pnpm dlx shadcn@latest add @constructive/billing-account pnpm dlx shadcn@latest add @constructive/account-phone-numbers pnpm dlx shadcn@latest add @constructive/agents-builder +pnpm dlx shadcn@latest add @constructive/coding-agent ``` `feature-pack-*` installs are standalone views and write their machine-readable diff --git a/apps/registry/scripts/smoke-install.ts b/apps/registry/scripts/smoke-install.ts index 9c133e05..eb8b7e7a 100644 --- a/apps/registry/scripts/smoke-install.ts +++ b/apps/registry/scripts/smoke-install.ts @@ -482,6 +482,63 @@ export function AiRegistrySmokeConsumer() { ], strictNullChecks: true, }, + { + name: 'coding-agent', + expected: [ + 'src/components/ui/coding-agent/browser-panel.tsx', + 'src/components/ui/coding-agent/code-surface-theme.ts', + 'src/components/ui/coding-agent/coding-agent-context.tsx', + 'src/components/ui/coding-agent/coding-agent.tsx', + 'src/components/ui/coding-agent/command-menu.tsx', + 'src/components/ui/coding-agent/demo.ts', + 'src/components/ui/coding-agent/home-view.tsx', + 'src/components/ui/coding-agent/index.ts', + 'src/components/ui/coding-agent/new-session.tsx', + 'src/components/ui/coding-agent/review-panel.tsx', + 'src/components/ui/coding-agent/session-composer.tsx', + 'src/components/ui/coding-agent/session-glyph.tsx', + 'src/components/ui/coding-agent/session-sidebar.tsx', + 'src/components/ui/coding-agent/session-tabs.tsx', + 'src/components/ui/coding-agent/session-timeline.tsx', + 'src/components/ui/coding-agent/session-view.tsx', + 'src/components/ui/coding-agent/shortcuts.ts', + 'src/components/ui/coding-agent/side-panel.tsx', + 'src/components/ui/coding-agent/terminal-panel.tsx', + 'src/components/ui/coding-agent/types.ts', + 'src/components/ui/coding-agent/use-harness.ts', + 'src/components/ui/coding-agent/harness.ts', + 'src/components/ui/coding-agent/harness-store.ts', + 'src/components/ui/coding-agent/agents-panel.tsx', + 'src/components/ui/coding-agent/worktree-picker.tsx', + 'src/components/ui/coding-agent/turn-rail.tsx', + 'src/components/ui/coding-agent/demo-invitations.ts', + 'src/components/ui/coding-agent/demo-security.ts', + 'src/components/ui/coding-agent/demo-long.ts', + 'src/components/ui/coding-agent/demo-browser.tsx', + 'src/components/ui/coding-agent/demo-shell.ts', + 'src/components/ui/coding-agent/file-picker-dialog.tsx', + 'src/components/ui/coding-agent/request-dock.tsx', + 'src/components/ui/coding-agent/run-presentation.tsx', + 'src/components/ui/coding-agent/session-header.tsx', + 'src/components/ui/coding-agent/session-layout.tsx', + 'src/components/ui/coding-agent/tab-strip.ts', + 'src/components/ui/coding-agent/use-draft-context.ts', + 'src/components/ui/coding-agent/use-panel-tabs.ts', + 'src/components/ui/coding-agent/session-surface.ts', + 'src/components/ui/coding-agent/workspace-tree.tsx', + 'src/components/ui/agents-builder/agent-status.tsx', + 'src/components/ui/agents-builder/mascot.tsx', + 'src/components/ui/agents-builder/playback.ts', + 'src/components/ui/ai/ask-card.tsx', + 'src/components/ui/ai/model-selector.tsx', + 'src/components/ui/workspace-kit/shell.tsx', + 'src/components/ui/workspace-kit/primitives.tsx', + 'src/components/ui/command.tsx', + 'src/components/ui/resizable.tsx', + ], + expectedPackages: ['@pierre/diffs', '@pierre/trees'], + strictNullChecks: true, + }, { name: 'data-rooms', expected: [ diff --git a/packages/ui/package.json b/packages/ui/package.json index f0cef7d3..32d7e825 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -845,6 +845,8 @@ "@base-ui/react": "^1.0.0", "@hookform/resolvers": "^5.2.2", "@internationalized/date": "^3.10.1", + "@pierre/diffs": "~1.5.2", + "@pierre/trees": "1.0.0-beta.6", "@remixicon/react": "^4.8.0", "@storybook/addon-a11y": "^10.1.11", "@storybook/react-vite": "^10.1.11", diff --git a/packages/ui/registry.json b/packages/ui/registry.json index 2d19508e..2eb048bd 100644 --- a/packages/ui/registry.json +++ b/packages/ui/registry.json @@ -233,6 +233,31 @@ "#__next, [data-nextjs-root-layout]": { "isolation": "isolate" }, + "@media (pointer: fine)": { + "*": { + "scrollbar-width": "thin", + "scrollbar-color": "color-mix(in oklch, var(--foreground) 14%, transparent) transparent" + }, + "::-webkit-scrollbar": { + "width": "10px", + "height": "10px" + }, + "::-webkit-scrollbar-track, ::-webkit-scrollbar-corner": { + "background": "transparent" + }, + "::-webkit-scrollbar-thumb": { + "background-color": "color-mix(in oklch, var(--foreground) 14%, transparent)", + "border": "3px solid transparent", + "background-clip": "content-box", + "border-radius": "9999px" + }, + "::-webkit-scrollbar-thumb:hover": { + "background-color": "color-mix(in oklch, var(--foreground) 20%, transparent)" + }, + "::-webkit-scrollbar-thumb:active": { + "background-color": "color-mix(in oklch, var(--foreground) 26%, transparent)" + } + }, "@media (prefers-reduced-motion: reduce)": { "*, *::before, *::after": { "scroll-behavior": "auto !important", @@ -414,6 +439,22 @@ "opacity": "0.95" } }, + "@keyframes agent-rail-travel": { + "from": { + "top": "-1rem" + }, + "to": { + "top": "100%" + } + }, + "@keyframes agent-pill-breathe": { + "0%, 100%": { + "transform": "scaleY(0.55)" + }, + "50%": { + "transform": "scaleY(1)" + } + }, "@keyframes ai-fade-up": { "from": { "opacity": "0", @@ -475,31 +516,6 @@ }, "[data-slot=\"portal-root\"] > *": { "pointer-events": "auto" - }, - "@media (pointer: fine)": { - "*": { - "scrollbar-width": "thin", - "scrollbar-color": "color-mix(in oklch, var(--foreground) 14%, transparent) transparent" - }, - "::-webkit-scrollbar": { - "width": "10px", - "height": "10px" - }, - "::-webkit-scrollbar-track, ::-webkit-scrollbar-corner": { - "background": "transparent" - }, - "::-webkit-scrollbar-thumb": { - "background-color": "color-mix(in oklch, var(--foreground) 14%, transparent)", - "border": "3px solid transparent", - "background-clip": "content-box", - "border-radius": "9999px" - }, - "::-webkit-scrollbar-thumb:hover": { - "background-color": "color-mix(in oklch, var(--foreground) 20%, transparent)" - }, - "::-webkit-scrollbar-thumb:active": { - "background-color": "color-mix(in oklch, var(--foreground) 26%, transparent)" - } } }, "files": [] @@ -3579,6 +3595,10 @@ "path": "registry/constructive/blocks/workspace-kit/menu.tsx", "type": "registry:component" }, + { + "path": "registry/constructive/blocks/workspace-kit/use-element-size.ts", + "type": "registry:lib" + }, { "path": "registry/constructive/blocks/workspace-kit/middle-truncate.tsx", "type": "registry:component" @@ -3785,6 +3805,211 @@ } ] }, + { + "name": "coding-agent", + "type": "registry:block", + "title": "Coding Agent", + "description": "A coding agent workspace template: sessions in a tab strip or a project sidebar, each with a chat transcript, a resizable side panel for reviewing diffs, reading files, previewing the app, following the session's other agents and its shared tasks, and a terminal dock. Sub-agents, orchestrator/worker runs and messages between agents show in the transcript, and sessions flag when any of their agents needs an answer.", + "categories": [ + "block", + "application", + "ai" + ], + "docs": "Import: `import { CodingAgent, CODING_AGENT_DEMO, createHarnessStore } from '@/components/ui/coding-agent'`. Pass your own `data`, handle side effects through `onAction`, and draw your app in the browser tab with `renderBrowserPage`. Each session plays a `script`, or shows a real harness: pass an `events` log, or a store from `createHarnessStore()` and push events as they happen. Sub-agents, messages between agents, handoffs, phases, shared tasks, worktrees, pull requests and CI checks are all events.", + "dependencies": [ + "lucide-react", + "motion", + "@pierre/diffs@~1.5.2", + "@pierre/trees@1.0.0-beta.6" + ], + "registryDependencies": [ + "cn", + "use-controllable-state", + "initials", + "ai", + "agents-builder", + "alert-dialog", + "workspace-kit", + "button", + "command", + "dropdown-menu", + "input", + "input-group", + "label", + "resizable", + "tabs", + "textarea", + "intl", + "popover", + "compose-refs" + ], + "files": [ + { + "path": "registry/constructive/blocks/coding-agent/browser-panel.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/code-surface-theme.ts", + "type": "registry:lib" + }, + { + "path": "registry/constructive/blocks/coding-agent/coding-agent-context.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/coding-agent.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/command-menu.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/demo.ts", + "type": "registry:lib" + }, + { + "path": "registry/constructive/blocks/coding-agent/home-view.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/index.ts", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/new-session.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/review-panel.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/session-composer.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/session-glyph.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/session-sidebar.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/session-tabs.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/session-timeline.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/session-view.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/shortcuts.ts", + "type": "registry:lib" + }, + { + "path": "registry/constructive/blocks/coding-agent/session-surface.ts", + "type": "registry:lib" + }, + { + "path": "registry/constructive/blocks/coding-agent/side-panel.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/terminal-panel.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/types.ts", + "type": "registry:lib" + }, + { + "path": "registry/constructive/blocks/coding-agent/workspace-tree.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/agents-panel.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/worktree-picker.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/turn-rail.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/demo-invitations.ts", + "type": "registry:lib" + }, + { + "path": "registry/constructive/blocks/coding-agent/demo-security.ts", + "type": "registry:lib" + }, + { + "path": "registry/constructive/blocks/coding-agent/demo-long.ts", + "type": "registry:lib" + }, + { + "path": "registry/constructive/blocks/coding-agent/demo-browser.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/demo-shell.ts", + "type": "registry:lib" + }, + { + "path": "registry/constructive/blocks/coding-agent/file-picker-dialog.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/request-dock.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/run-presentation.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/session-header.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/session-layout.tsx", + "type": "registry:component" + }, + { + "path": "registry/constructive/blocks/coding-agent/tab-strip.ts", + "type": "registry:lib" + }, + { + "path": "registry/constructive/blocks/coding-agent/use-draft-context.ts", + "type": "registry:lib" + }, + { + "path": "registry/constructive/blocks/coding-agent/use-panel-tabs.ts", + "type": "registry:lib" + }, + { + "path": "registry/constructive/blocks/coding-agent/harness-store.ts", + "type": "registry:lib" + }, + { + "path": "registry/constructive/blocks/coding-agent/harness.ts", + "type": "registry:lib" + }, + { + "path": "registry/constructive/blocks/coding-agent/use-harness.ts", + "type": "registry:lib" + } + ] + }, { "name": "data-rooms", "type": "registry:block", @@ -4420,7 +4645,9 @@ "spinner", "command", "popover", - "kbd" + "kbd", + "fluid-highlight", + "compose-refs" ], "files": [ { diff --git a/packages/ui/src/components/agents-builder/agent-run-panel.tsx b/packages/ui/src/components/agents-builder/agent-run-panel.tsx index 35720d6a..811cac9f 100644 --- a/packages/ui/src/components/agents-builder/agent-run-panel.tsx +++ b/packages/ui/src/components/agents-builder/agent-run-panel.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Bot, Check, ChevronRight, CircleAlert, Clock3, Hand, MessageCircleQuestion, RotateCcw, Sparkles, Square, Zap } from 'lucide-react'; +import { Bot, Check, ChevronRight, RotateCcw, Sparkles, Square, Zap } from 'lucide-react'; import * as React from 'react'; import { cn } from '../../lib/utils'; @@ -17,6 +17,7 @@ import { ThinkingStatus } from '../ai/thinking-status'; import { ToolTrace } from '../ai/tool-trace'; import { Button } from '../button'; import { Spinner } from '../spinner'; +import { type StatusSignal, StatusGlyph, StatusLabel } from './agent-status'; import { useAgentsBuilder } from './agents-builder-context'; import { Composer } from './composer'; import { IntegrationMark } from './integration-mark'; @@ -25,30 +26,28 @@ import { useLatest } from '../workspace-kit/use-latest'; import { RevealText } from './reveal-text'; import { usePlaybackClock } from './playback'; import type { AgentRunScript, RunApproval, RunFailure, RunQuery, RunQuestion, ToolStep } from './types'; -import { type AgentRunState, RUN_TIMING, type RunGroupPlay } from './use-agent-run'; +import { type AgentRunState, RUN_TIMING, type RunGroupPlay, runProgress } from './use-agent-run'; const GROUP_ICON = { subagents: , skills: } as const; const GROUP_NOUN = { subagents: ['agent', 'agents'], skills: ['skill', 'skills'] } as const; -// Status lines for the cards a run stops on. Static, so they are built once. -const APPROVAL_EYEBROW = ( - <> -