diff --git a/doc/plans/2026-10-06-default-agent-runner.md b/doc/plans/2026-10-06-default-agent-runner.md index df07d0190a6..13a460f9b55 100644 --- a/doc/plans/2026-10-06-default-agent-runner.md +++ b/doc/plans/2026-10-06-default-agent-runner.md @@ -1703,3 +1703,12 @@ Actual master252d9b5077f4eee98ec48225641ef1a02d824158 is integrated through an o On the owned staging stack, ordinary environment Test connects to the immutable image but cannot confirm probe-lease cleanup. New-agent discovery remains Legacy; the supported onboarding entry surfaces the exact pending-cleanup error before hiring. This is a blocked managed acceptance, not a native pass or a silent runner conversion. The QA environment had autoDeleteInterval0; source ordering makes immediate deletion after stop before archival a plausible cause, not a proven provider diagnosis. Attempting to change its QA interval to60 is correctly rejected while cleanup is pending, so persisted configuration remains0. Preserve the failed lease, its holds and the guard; do not clear durable state or extract provider credentials to manufacture qualification. Next action is confirmed owned-resource cleanup, then qualification of corrected QA configuration and normal native setup/task/follow-up. Combined-source integration passes100 existing controls with the repository launcher's canonical isolated HOME/config/TMPDIR contract:41 device-login,12 real-PostgreSQL instance-secret and47 lifecycle tests, zero skips. Initial direct-invocation99/100 and alone identity-verdict failures remain recorded; no expectations or product code were changed. No on-disk worktree config write is observed, and3136 uses a separate preserved home/data. Direct server/E2E TypeScript pass. The existing signoff failure assertion now retains only a bounded verified errorCode; ownership, issue checks, deadlines, retry semantics and terminal failures stay intact. Actual-helper baseline12-pass/4-missing-diagnostic failures become16 passing inert controls, with no browser/provider work. This improves subsequent diagnosis and does not claim to repair the earlier cancellation. + + +The integrated6c candidate now passes all48 ordinary hosted jobs, required verify/E2E and fresh Greptile5/5 with26 resolved threads. The earlier signoff case passes on this combined source; its earlier0065 cancellation remains an inconclusive historical failure, not a proved flake. The remote four-platform/package qualification38445419339 binds literal6c. A single guarded managed onboarding cell38448001365 also resolves literal6c through the trusted master workflow and independently verified target lock; its exact selection and180-second/concurrency1 guard pass admission. No second paid cell or automatic test rerun is dispatched. + +Master advances once more to c8efdbfba43377d39f57e4822e92d796a8a044b6 through Storybook-only #16196. Its only conflicts are the same three catalog files. Semantic reconciliation preserves all226 candidate suites and37 new master suites,263 suites/2499 stories, stable IDs and both review histories. Existing27 catalog/generator controls and35-area/175-ID map checks pass. Independent virtual-merge and actual staged checks prove every production/native/package/build component remains byte-identical to6c. Keep the6c package/image/live receipts literal; this catalog-only merge requires fresh head CI/review and applicable Storybook checks, not another expensive native build or paid runtime campaign. + +The supported Cloud CLI is re-read from current private master8472ece87f8d9a9e17a6116b9f4631d98ce63486 and is byte-identical to the previously used maintained command. Its one-target preview request d227da49-fd73-44c0-9377-f25711e529bc builds literal6c remotely in38448052890, preserving the existing pinned/user-managed QA stack and leaving fleet/default promotion disabled. Build, deployment and actual serving-source verification remain pending. A separate QA environment with the same immutable execution image and autoDeleteInterval60 is saved through ordinary UI, with instance default unchanged and no probe yet. The prior failed environment, durable teardown record and cost holds remain intact. It cannot borrow the older environment's qualification facts; it needs its own new saved probe and confirmed matching cleanup before automatic native onboarding. + +Existing ordinary board lease APIs and CLI diagnostics are identified, but anonymous challenge creation and the installed CLI's existing board context both receive401 at this tenant's public edge. No browser/provider credential is extracted and no new inspection API/workflow is introduced. The plugin settings route correctly honors Cloud's hidden-settings gate. Existing Fleet Admin provider links reach the normal signed-in Railway log UI; two successful aggregate orphan-cleanup sweeps prove the controller operates, but neither receipt binds the exact outstanding probe lease. Preserve that uncertainty and its hold. Current-source installed JS/DB migration/account preservation, independent managed task/follow-up and normal protected merge remain the next gates. No timer or production deployment is started. diff --git a/maps/storybook-map/INDEX.md b/maps/storybook-map/INDEX.md index 685062428c2..6efcd072ddb 100644 --- a/maps/storybook-map/INDEX.md +++ b/maps/storybook-map/INDEX.md @@ -4,7 +4,7 @@ [Contract and maintenance](README.md) · [Review findings](REVIEW.md) -226 story files; 2242 indexed stories. Counts describe inventory, not passed tests or complete feature coverage. +263 story files; 2499 indexed stories. Counts describe inventory, not passed tests or complete feature coverage. Status describes product alignment; implementation describes the rendered subject; boundary describes simulated data. Sources and limitations are in catalog.json. Related capabilities cross-list one canonical story file. @@ -21,6 +21,7 @@ Instance setup, company wizard, first agent and task. | [Getting started and access/Onboarding and first work/Onboarding/Agent arc](../../ui/storybook/stories/onboarding/onboarding-agent-arc.stories.tsx) | `onboarding/adapter`, `onboarding/first-task` | unreviewed / production / fixtures | 31 / 11 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Codex selection/branding and upstream recipe source changes inspected; prior parity limitations remain, with no new rendered or live proof. Four managed Codex cases use scoped immutable-image discovery and a stateful saved-probe fixture: native default, measured Legacy, permission refusal, and unconfirmed cleanup. Native isolation recipe added; these fixture setup responses do not establish server-resolved transport isolation, real sandbox cleanup, or local/SSH enforcement. The operator QA timeout/concurrency recipe was inspected; this preview does not verify the first-task fixture’s pre-admission runtime bounds. | | [Getting started and access/Onboarding and first work/Onboarding/Character](../../ui/storybook/stories/onboarding/onboarding-character.stories.tsx) | `onboarding/company` | unreviewed / production / fixtures | 3 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Codex selection/branding and upstream recipe source changes inspected; prior parity limitations remain, with no new rendered or live proof. The added native isolation recipe is outside this character preview’s demonstrated scope. The operator QA timeout/concurrency recipe was inspected; this preview does not verify the first-task fixture’s pre-admission runtime bounds. | | [Getting started and access/Onboarding and first work/Onboarding/Connect a model](../../ui/storybook/stories/onboarding/onboarding-connect-model.stories.tsx) | `onboarding/adapter`, `onboarding/first-task` | historical / prototype / fixtures | 5 / 0 | Older tile/mode preview; onboarding-agent-arc mounts the production wizard. Codex selection/branding and upstream recipe source changes inspected; prior parity limitations remain, with no new rendered or live proof. The expanded native isolation recipe is not exercised by this retained historical tile preview. The operator QA timeout/concurrency recipe was inspected; this preview does not verify the first-task fixture’s pre-admission runtime bounds. | +| [Getting started and access/Onboarding and first work/Resume a saved draft](../../ui/storybook/stories/onboarding/resume-draft.stories.tsx) | `onboarding/resume` | current / production / fixtures | 4 / 3 | OnboardingWizard restores a seeded draft, malformed/unavailable-company handling and draft retention across a fixture remount. The reopen button belongs to the fixture host; real reload, account switch and storage failure remain separate checks. The merged wizard includes Codex harness/runner draft restoration and immutable-environment setup qualification. These isolated draft states do not establish the new native setup, task/follow-up, account or cleanup journey. | ### Login, invitations, and access @@ -30,6 +31,8 @@ Sessions, bootstrap, membership, roles, and CLI authorization. | Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note | | --- | --- | --- | --- | --- | +| [Getting started and access/Login, invitations, and access/CLI authorization](../../ui/storybook/stories/access/cli-authorization.stories.tsx) | `access/cli-auth` | current / production / fixtures | 9 / 5 | Real CliAuth route with pending/expired/unavailable/preflight denial, approval/cancel and write pending/error. Fictional challenge; no CLI identity or real token is authorized. | +| [Getting started and access/Login, invitations, and access/Organization membership](../../ui/storybook/stories/access/membership.stories.tsx) | `access/membership` | current / production / fixtures | 7 / 2 | CompanyAccess member list and role change with loading/empty/failure/403 and failed mutation toast. Backend actor authority, invite lifecycle and durable membership are not verified. | | [Getting started and access/Login, invitations, and access/Private tasks/00 Review guide](../../ui/storybook/stories/access/private-tasks/00-review-guide.stories.tsx) | `access/resources`, `tasks/properties` | unreviewed / guide / static | 1 / 0 | Review guide retained; explanatory content does not establish runtime or production coverage. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. | | [Getting started and access/Login, invitations, and access/Private tasks/01 Creation](../../ui/storybook/stories/access/private-tasks/01-creation.stories.tsx) | `access/resources`, `tasks/properties` | unreviewed / production / fixtures | 22 / 14 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Master reconciliation inspected assignment-history recipient suggestions and manual-selection retention; existing stories do not establish the new assignment-history behavior. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation visibility now defaults to hiding automated independent tasks, with a show agent-created tasks control and inbox participation exceptions. Source alignment reviewed; these fixture stories do not establish live server pagination or authorization. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. | | [Getting started and access/Login, invitations, and access/Private tasks/02 Sharing](../../ui/storybook/stories/access/private-tasks/02-sharing.stories.tsx) | `access/resources`, `tasks/properties` | unreviewed / production / fixtures | 26 / 10 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. | @@ -38,6 +41,7 @@ Sessions, bootstrap, membership, roles, and CLI authorization. | [Getting started and access/Login, invitations, and access/Private tasks/05 Projects](../../ui/storybook/stories/access/private-tasks/05-projects.stories.tsx) | `access/resources`, `tasks/properties`, `projects/lifecycle` | unreviewed / production / fixtures | 19 / 12 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. | | [Getting started and access/Login, invitations, and access/Private tasks/06 Full product pages](../../ui/storybook/stories/access/private-tasks/06-full-product-pages.stories.tsx) | `access/resources`, `tasks/properties`, `projects/lifecycle` | unreviewed / production / fixtures | 21 / 12 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Master reconciliation inspected assignment-history recipient suggestions and manual-selection retention; existing stories do not establish the new assignment-history behavior. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation visibility now defaults to hiding automated independent tasks, with a show agent-created tasks control and inbox participation exceptions. Source alignment reviewed; these fixture stories do not establish live server pagination or authorization. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. | | [Getting started and access/Login, invitations, and access/Private tasks/07 User stories](../../ui/storybook/stories/access/private-tasks/07-user-stories.stories.tsx) | `access/resources`, `tasks/properties` | unreviewed / production / fixtures | 13 / 7 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Master reconciliation inspected assignment-history recipient suggestions and manual-selection retention; existing stories do not establish the new assignment-history behavior. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation visibility now defaults to hiding automated independent tasks, with a show agent-created tasks control and inbox participation exceptions. Source alignment reviewed; these fixture stories do not establish live server pagination or authorization. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. | +| [Getting started and access/Login, invitations, and access/Sign in](../../ui/storybook/stories/access/sign-in.stories.tsx) | `access/login` | current / production / fixtures | 6 / 3 | AuthPage email form, session read loading/error, submission pending/failure and navigation to Companies after a fixture session. Real authentication, bootstrap claim and sign-out remain separate. | ### Companies and portability @@ -47,6 +51,8 @@ Company switching/settings, archival, package import/export. | Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note | | --- | --- | --- | --- | --- | +| [Getting started and access/Companies and portability/Import preview](../../ui/storybook/stories/companies/company-import.stories.tsx) | `companies/portability` | current / production / fixtures | 6 / 5 | Shipped CompanyImport GitHub-source preview, warnings, pending, error and 403. Plan/package replies are fictional; export, final import and real package fidelity remain separate scenarios/contracts. The merged production import page includes Codex harness/runner selection and preservation of native settings. This fixture review does not prove automatic native execution, credential reuse, import persistence or provider behavior. | +| [Getting started and access/Companies and portability/Selection and archived organizations](../../ui/storybook/stories/companies/company-selection.stories.tsx) | `companies/selection`, `companies/archive` | current / production / fixtures | 7 / 2 | Companies list, selection storage and archived-company restoration with loading/empty/error/403. This covers the archive list/restoration side; CompanySettings archive confirmation remains an additional UI scenario. | | [Getting started and access/Companies and portability/Decision models/01 Settings](../../ui/storybook/stories/companies/decision-models/01-settings.stories.tsx) | `companies/identity`, `budgets-costs/cost-reporting`, `companies/decision-models` | unreviewed / production / fixtures | 11 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Related decision-service examples show connection/model configuration, bounded test receipts or invocation history using shipped subjects and simulated data. No paid provider or authorization proof. | | [Getting started and access/Companies and portability/Decision models/02 History](../../ui/storybook/stories/companies/decision-models/02-history.stories.tsx) | `companies/identity`, `budgets-costs/cost-reporting`, `companies/decision-models` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Related decision-service examples show connection/model configuration, bounded test receipts or invocation history using shipped subjects and simulated data. No paid provider or authorization proof. | | [Getting started and access/Companies and portability/Decision models/03 Journey](../../ui/storybook/stories/companies/decision-models/03-journey.stories.tsx) | `companies/identity`, `budgets-costs/cost-reporting`, `companies/decision-models` | unreviewed / production / fixtures | 3 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Related decision-service examples show connection/model configuration, bounded test receipts or invocation history using shipped subjects and simulated data. No paid provider or authorization proof. The upstream budget UI distinguishes soft warnings from hard stops; hard incidents retain raise-and-resume and keep-paused controls. | @@ -110,6 +116,8 @@ Personal triage, decision queues, unread/blocked views, search, and personal tas | [Tasks and conversations/Inbox, decisions, and search/Product/Inbox/Blocked tab](../../ui/storybook/stories/inbox-search/blocked-inbox.stories.tsx) | `inbox-search/triage`, `recovery/diagnosis` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Creation visibility now defaults to hiding automated independent tasks, with a show agent-created tasks control and inbox participation exceptions. Source alignment reviewed; these fixture stories do not establish live server pagination or authorization. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. | | [Tasks and conversations/Inbox, decisions, and search/Decisions/DecisionCard](../../ui/storybook/stories/inbox-search/decision-card.stories.tsx) | `inbox-search/decisions` | unreviewed / production / fixtures | 9 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. | | [Tasks and conversations/Inbox, decisions, and search/Pages/Decisions Desk](../../ui/storybook/stories/inbox-search/decisions-desk.stories.tsx) | `inbox-search/decisions` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. | +| [Tasks and conversations/Inbox, decisions, and search/Personal task views](../../ui/storybook/stories/inbox-search/inbox-views.stories.tsx) | `inbox-search/views` | current / production / fixtures | 8 / 0 | Shipped Inbox host with Mine/Recent/Unread/Blocked under combined-inbox flags and list states. Approvals/join requests/run lists are isolated alongside tasks. Failed/denied list queries currently fall back to Inbox zero; personal participation/read metadata and server counts remain production verification. Mine/recent fixture tasks are human-created; the upstream agent-created-task filter changes are separate visibility scenarios. | +| [Tasks and conversations/Inbox, decisions, and search/Personal task notifications](../../ui/storybook/stories/inbox-search/personal-task-toasts.stories.tsx) | `inbox-search/toasts` | current / production / fixtures | 4 / 4 | LiveUpdatesProvider and ToastViewport consume fixture socket eligibility, suppress unrelated/visible-task events and deduplicate replay. Server eligibility metadata is simulated; no persisted multiplayer participation proof. The upstream Inbox recipe adds agent-created-task discovery checks; this LiveUpdatesProvider suite still tests toast eligibility only. | | [Tasks and conversations/Inbox, decisions, and search/Product/Search & Command K](../../ui/storybook/stories/inbox-search/search.stories.tsx) | `inbox-search/search` | unreviewed / prototype / fixtures | 1 / 0 | Local search-page composition mounts production result components; does not establish parity with the production Search route. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. | | [Tasks and conversations/Inbox, decisions, and search/Pages/Decisions](../../ui/storybook/stories/inbox-search/what-needs-me.stories.tsx) | `inbox-search/decisions` | unreviewed / production / fixtures | 13 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. | @@ -125,6 +133,7 @@ Persistent conversations, discovery, handoff, and gated board chat. | [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat](../../ui/storybook/stories/agent-chat/agent-chat.stories.tsx) | `agent-chat/discovery`, `agent-chat/history`, `agent-chat/handoff` | mixed / mixed / fixtures | 17 / 0 | Production conversation/task routes with a local ChatEntryLanding. Default-agent behavior and fallback API responses remain outside current-product parity. Master reconciliation inspected assignment-history recipient suggestions and manual-selection retention; existing stories do not establish the new assignment-history behavior. ProjectCreated, ExistingProject, MultipleRepositories, ProjectWithoutRepository and ProjectCreationFailed show linked handoff results/navigation through shipped chat/task hosts. Tool execution and persistence are simulated. Codex selection/branding and upstream recipe source changes inspected; prior parity limitations remain, with no new rendered or live proof. | | [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat sidebar/Components](../../ui/storybook/stories/agent-chat/agent-conversation-sidebar.stories.tsx) | `agent-chat/discovery`, `agent-chat/history` | current / production / fixtures | 10 / 1 | Support module re-exports production AgentConversationSidebar; search/selection use fixture callbacks. | | [Tasks and conversations/Agent conversations and project handoff/Design explorations/Chat entry](../../ui/storybook/stories/agent-chat/chat-entry.stories.tsx) | `agent-chat/discovery`, `agent-chat/history` | historical / mixed / fixtures | 9 / 3 | Retained first-created-agent sidebar/landing design predates personal primary-agent selection. Shared production conversation components remain useful. Master reconciliation inspected assignment-history recipient suggestions and manual-selection retention; existing stories do not establish the new assignment-history behavior. Codex selection/branding and upstream recipe source changes inspected; prior parity limitations remain, with no new rendered or live proof. | +| [Tasks and conversations/Agent conversations and project handoff/Conference Room](../../ui/storybook/stories/agent-chat/conference-room.stories.tsx) | `agent-chat/conference-room` | current / production / fixtures | 5 / 4 | BoardChat empty, reply, send pending/error/403 through fixture SSE and persisted comment reads. 403 uses the shipped generic unavailable message; route gates and real Claude execution remain separate. | ### Questions and approvals @@ -151,7 +160,9 @@ Follow-ups, queue edits, interruption, and pause/resume. | Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note | | --- | --- | --- | --- | --- | +| [Tasks and conversations/Steering and queued messages/Draft recovery](../../ui/storybook/stories/steering/composer-draft-recovery.stories.tsx) | `steering/drafts` | current / production / fixtures | 6 / 3 | TaskChatComposer uses owned task-keyed browser drafts, uncertain submission fence, switch/restore and failed/accepted send callbacks. API receipt authority and real submission durability remain separate. Storybook-owned draft keys are reset using the matching retained submission ID on setup and cleanup; same-browser uncertain-to-empty/send switching is covered separately. | | [Tasks and conversations/Steering and queued messages/Composer/Steering and queued messages](../../ui/storybook/stories/steering/composer-queued-messages.stories.tsx) | `steering/queue`, `steering/edit-order-discard`, `steering/steer`, `steering/interrupt` | current / production / fixtures | 6 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Master reconciliation inspected assignment-history recipient suggestions and manual-selection retention; existing stories do not establish the new assignment-history behavior. | +| [Tasks and conversations/Steering and queued messages/Accepted response delivery](../../ui/storybook/stories/steering/durable-response-queue.stories.tsx) | `steering/approval-queue`, `recovery/user-continuation` | current / production / fixtures | 7 / 6 | Accepted immutable response, cleanup wait, unavailable admission, empty queue and interrupt delivery states. Local callbacks remove delivered entries; durable queue/coalescing and fresh-session authority need production tests. The expanded native copyback recovery recipe requires harness stop proof; these generic exhaustion/queued-response fixtures do not establish that path. The shipped pending-delivery path removes rows optimistically and shows no separate progress indicator; the Delivering story asserts that actual presentation. | | [Tasks and conversations/Steering and queued messages/Surfaces/Interrupt Handoff](../../ui/storybook/stories/steering/interrupt-handoff.stories.tsx) | `steering/interrupt` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Tasks and conversations/Steering and queued messages/Composer/Paused task takeover](../../ui/storybook/stories/steering/paused-composer.stories.tsx) | `steering/stop-and-pause` | historical / prototype / fixtures | 8 / 0 | Retained local paused takeover; task-execution-controls exercises the shipped pause/stop controls. Master reconciliation inspected assignment-history recipient suggestions and manual-selection retention; existing stories do not establish the new assignment-history behavior. | | [Tasks and conversations/Steering and queued messages/Composer/Execution controls](../../ui/storybook/stories/steering/task-execution-controls.stories.tsx) | `steering/stop-and-pause` | current / production / fixtures | 22 / 6 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Master reconciliation inspected assignment-history recipient suggestions and manual-selection retention; existing stories do not establish the new assignment-history behavior. | @@ -178,6 +189,7 @@ Versioned documents, annotations, files, outputs, and artifact library. | [Tasks and conversations/Documents, attachments, and work products/Components/CSV File Tab](../../ui/storybook/stories/documents-artifacts/csv-preview.stories.tsx) | `documents-artifacts/attachments` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Tasks and conversations/Documents, attachments, and work products/Product/Documents/Annotations](../../ui/storybook/stories/documents-artifacts/document-annotations.stories.tsx) | `documents-artifacts/annotations` | unreviewed / production / fixtures | 9 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Tasks and conversations/Documents, attachments, and work products/Product/Document Comments](../../ui/storybook/stories/documents-artifacts/document-comments.stories.tsx) | `documents-artifacts/annotations` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | +| [Tasks and conversations/Documents, attachments, and work products/Document revisions](../../ui/storybook/stories/documents-artifacts/document-revisions.stories.tsx) | `documents-artifacts/documents` | current / production / fixtures | 9 / 4 | IssueDocumentsSection renders current/edit/historical preview and append-only restore with mutation error. Read/loading failures retain shipped fallback rendering; no persisted concurrency or authorization proof. | | [Tasks and conversations/Documents, attachments, and work products/Components/File Viewer Sheet](../../ui/storybook/stories/documents-artifacts/file-viewer.stories.tsx) | `documents-artifacts/attachments` | unreviewed / production / fixtures | 13 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/01 Preview](../../ui/storybook/stories/documents-artifacts/html-artifacts/01-preview.stories.tsx) | `documents-artifacts/attachments`, `workspaces/files-git` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. | | [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/02 Workspace](../../ui/storybook/stories/documents-artifacts/html-artifacts/02-workspace.stories.tsx) | `documents-artifacts/attachments`, `workspaces/files-git` | unreviewed / production / fixtures | 3 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. | @@ -259,6 +271,7 @@ Discovery, sources, authoring, revisions, tests, and agent policies. | [Agents and reusable capabilities/Skills and Skill Studio/Skills/Repository packages](../../ui/storybook/stories/skills/repository-skill-packages/repository-skill-packages.stories.tsx) | `skills/versions`, `skills/discovery` | unreviewed / production / fixtures | 9 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Agents and reusable capabilities/Skills and Skill Studio/Skills/Import progress](../../ui/storybook/stories/skills/skill-import-progress.stories.tsx) | `skills/versions`, `skills/discovery` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Agents and reusable capabilities/Skills and Skill Studio/Product/Skills/Policy surfaces](../../ui/storybook/stories/skills/skill-policy-surfaces.stories.tsx) | `skills/policy` | unreviewed / production / fixtures | 2 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | +| [Agents and reusable capabilities/Skills and Skill Studio/Test runs](../../ui/storybook/stories/skills/skill-test-runs.stories.tsx) | `skills/testing` | current / production / fixtures | 9 / 1 | SkillStudio test history statuses and result detail with immutable fixture output/version. No agent/provider runs; authoring, harness execution, persisted costs and snapshots require production tests. | | [Agents and reusable capabilities/Skills and Skill Studio/Skills Store/Skill detail](../../ui/storybook/stories/skills/skills-store-detail.stories.tsx) | `skills/discovery`, `skills/versions`, `skills/policy` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Related examples cover fixture revisions and agent attachment selection. Save and attachment submission callbacks are no-ops; durable authoring, grants and enforcement are not established. | | [Agents and reusable capabilities/Skills and Skill Studio/Skills Store/Discovery grid](../../ui/storybook/stories/skills/skills-store-discovery.stories.tsx) | `skills/discovery` | unreviewed / production / fixtures | 3 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | @@ -293,8 +306,9 @@ Goal hierarchy, ownership, status, and project/task context. [Feature recipe](../feature-map/goals.md) -No primary story suite. Check capability lookup below for related examples; this does not imply a missing product feature. - +| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note | +| --- | --- | --- | --- | --- | +| [Projects and execution/Goals and work alignment/Aligned work and navigation](../../ui/storybook/stories/goals/goal-alignment.stories.tsx) | `goals/alignment`, `goals/navigation` | current / production / fixtures | 6 / 2 | GoalDetail child navigation and aligned-project tab with empty/loading/failure/403. Current UI displays relationships but has no project goal-assignment control; no invented assignment UI. | ### Workspaces, services, and files @@ -308,6 +322,7 @@ Provisioning, task bindings, services/logs, Git/files, and closure. | [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Footer](../../ui/storybook/stories/workspaces/browser-use-footer.stories.tsx) | `workspaces/services`, `tasks/discussion` | unreviewed / production / fixtures | 21 / 2 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. | | [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Panel](../../ui/storybook/stories/workspaces/browser-use.stories.tsx) | `workspaces/services`, `tasks/discussion` | unreviewed / production / fixtures | 28 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Creation-visibility verification recipes were updated; this story scenario is unchanged and does not prove that journey. The filter uses one lowercase label without helper text. All chat-channel tasks count as bot-created; the focused visibility stories cover human-initiated channel work. | | [Projects and execution/Workspaces, services, and files/Components/SearchableSelect/Workspace picker](../../ui/storybook/stories/workspaces/searchable-select.stories.tsx) | `workspaces/binding` | unreviewed / production / fixtures | 9 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. | +| [Projects and execution/Workspaces, services, and files/Close readiness](../../ui/storybook/stories/workspaces/workspace-close.stories.tsx) | `workspaces/closure` | current / production / fixtures | 7 / 7 | Readiness dialog with loading, inspection failure/403, running-task block and pending/accepted archive response. No real workspace cleanup, Git removal or service shutdown is performed. | | [Projects and execution/Workspaces, services, and files/Issue/Workspace File Browser](../../ui/storybook/stories/workspaces/workspace-file-browser.stories.tsx) | `workspaces/files-git` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. | | [Projects and execution/Workspaces, services, and files/Workspaces/Runtime provisioning status](../../ui/storybook/stories/workspaces/workspace-runtime-provisioning.stories.tsx) | `workspaces/provisioning` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed ExecutionWorkspaceDetail null-project handling. This suite mounts only the unchanged RuntimeProvisionStatusValue inside a local context row; it does not exercise the full detail page or projectless queries. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. | | [Projects and execution/Workspaces, services, and files/Workspaces/Service control bar](../../ui/storybook/stories/workspaces/workspace-service-control-bar.stories.tsx) | `workspaces/services` | unreviewed / production / fixtures | 16 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the expanded workspace recipe; these examples do not establish durable task-folder continuity, repository authorization or sandbox admission. The workspace recipe now requires child boundary, safe reopen and checkpoint ancestry controls; these backend checks are not established by this fixture. The provisioning recipe requires controller script denial for remote selections and copy-only local source choices, including retained denial through metadata changes and reopening; these fixtures do not verify execution placement. Canonical default provisioning follows configured policy, primary, creation time and ID; these fixtures do not run source hooks or establish this backend precedence. | @@ -318,8 +333,10 @@ Local, SSH, sandbox providers, target probes, and custom images. [Feature recipe](../feature-map/execution-environments.md) -No primary story suite. Check capability lookup below for related examples; this does not imply a missing product feature. - +| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note | +| --- | --- | --- | --- | --- | +| [Projects and execution/Execution environments/Custom images](../../ui/storybook/stories/execution-environments/custom-images.stories.tsx) | `execution-environments/images` | current / production / fixtures | 6 / 3 | CompanyEnvironments image metadata: unconfigured, captured, unsupported, failed, loading and 403. No live setup terminal, provider capture, rollback or relink is executed. | +| [Projects and execution/Execution environments/Configuration, capabilities, and probes](../../ui/storybook/stories/execution-environments/environment-states.stories.tsx) | `execution-environments/configuration`, `execution-environments/capabilities`, `execution-environments/probe` | current / production / fixtures | 10 / 4 | CompanyEnvironments configuration/edit, unsupported capabilities and probe pending/failure/success. No provider contacted. List read/loading failures expose shipped fallback behavior, not dedicated error UI. | ### Recovery @@ -334,6 +351,7 @@ Stopped work, workspace repair, bounded continuation, and reconnect. | [Projects and execution/Recovery/Tasks/Execution recovery](../../ui/storybook/stories/recovery/execution-recovery.stories.tsx) | `recovery/diagnosis` | unreviewed / production / fixtures | 18 / 2 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Projects and execution/Recovery/Product/Issue/Blocked notice](../../ui/storybook/stories/recovery/issue-blocked-notice.stories.tsx) | `recovery/diagnosis`, `task-coordination/dependencies` | unreviewed / production / fixtures | 7 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Projects and execution/Recovery/Tasks/Model capacity retry](../../ui/storybook/stories/recovery/model-capacity-retry.stories.tsx) | `recovery/bounded-continuation` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | +| [Projects and execution/Recovery/Exhaustion and user decisions](../../ui/storybook/stories/recovery/recovery-gap-states.stories.tsx) | `recovery/exhaustion`, `recovery/unknown-outcome` | current / production / fixtures | 7 / 7 | Exhaustion notice, blocked authority, retry pending/failure/accepted and inactive notice; unknown-outcome action card is rendered separately. Retry callbacks and disposition context are simulated; no real continuation proof. The expanded native copyback recovery recipe requires harness stop proof; these generic exhaustion/queued-response fixtures do not establish that path. | | [Projects and execution/Recovery/Design previews/Recovery notice](../../ui/storybook/stories/recovery/recovery-notice-preview.stories.tsx) | `recovery/diagnosis`, `recovery/source-action` | current / production / fixtures | 9 / 3 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Projects and execution/Recovery/Product/Issue Scheduled retry surfaces](../../ui/storybook/stories/recovery/scheduled-retry.stories.tsx) | `recovery/bounded-continuation` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Projects and execution/Recovery/Paperclip/Source Issue Recovery](../../ui/storybook/stories/recovery/source-issue-recovery.stories.tsx) | `recovery/diagnosis`, `recovery/source-action` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Source review confirms ordinary recovery fixtures remain applicable after the native-owner resolution gate; native-owner stop verification has its own suite. | @@ -384,6 +402,7 @@ Agent grants, off/ask/allowed actions, discovery, tests, and revocation. | [Apps, channels, and extensions/App access and action permissions/Tools & Access/ActionCard](../../ui/storybook/stories/app-permissions/action-card.stories.tsx) | `app-permissions/test-action`, `app-permissions/action-policy` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Apps, channels, and extensions/App access and action permissions/Apps/Action permissions test](../../ui/storybook/stories/app-permissions/action-permissions-test.stories.tsx) | `app-permissions/test-action`, `app-permissions/action-policy` | unreviewed / production / fixtures | 7 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Apps, channels, and extensions/App access and action permissions/Apps/Managed accounts](../../ui/storybook/stories/app-permissions/apps-managed-accounts.stories.tsx) | `app-permissions/account-maintenance` | current / production / fixture-server | 1 / 0 | Production Browse requires owned tests/aggregator-accounts/test-drive.ts on localhost:4310; providers are simulated. Not an offline story. | +| [Apps, channels, and extensions/App access and action permissions/Review newly discovered actions](../../ui/storybook/stories/app-permissions/catalog-review.stories.tsx) | `app-permissions/catalog-change` | current / production / fixtures | 7 / 2 | Shipped ActionsSection quarantine reviewer with allowed/blocked decisions, empty/refreshing/saving/read-only props. Callbacks simulate catalog persistence; discovery failures belong to AppDetail and backend authority remains separate. | | [Apps, channels, and extensions/App access and action permissions/Tools/Permitted vs Installed](../../ui/storybook/stories/app-permissions/permitted-vs-installed.stories.tsx) | `app-permissions/agent-access`, `app-permissions/action-policy` | unreviewed / production / fixtures | 12 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | ### External chat and email @@ -397,6 +416,7 @@ Provider-specific identity, threads, files, delivery, and endpoint upkeep. | [Apps, channels, and extensions/External chat and email/Connections/AgentMail setup](../../ui/storybook/stories/chat-channels/agentmail-onboarding.stories.tsx) | `chat-channels/providers`, `connection-setup/channel-setup` | unreviewed / production / fixtures | 19 / 16 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the added connection/destination-default recipe; these unchanged setup, identity or conversation examples do not establish default selection or new-task inheritance. The recipe adds bounded Telegram subscription maintenance regressions; this story does not establish real provider startup recovery. | | [Apps, channels, and extensions/External chat and email/Connections/AgentMail settings](../../ui/storybook/stories/chat-channels/agentmail-settings.stories.tsx) | `chat-channels/lifecycle` | unreviewed / production / fixtures | 8 / 5 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation: EmailEndpointSettings remains the mounted inbox subsection. The surrounding ChatEndpointDetail connection-default editor and its save/inheritance behavior are not represented here. | | [Apps, channels, and extensions/External chat and email/Connections/AgentMail tasks](../../ui/storybook/stories/chat-channels/agentmail.stories.tsx) | `chat-channels/conversation` | unreviewed / prototype / fixtures | 9 / 2 | Local EmailTaskExperience combines production composer/bubbles with simulated email lifecycle; compare against IssueDetail before claiming page parity. Task-workspace reconciliation reviewed the added connection/destination-default recipe; these unchanged setup, identity or conversation examples do not establish default selection or new-task inheritance. Master reconciliation inspected assignment-history recipient suggestions and manual-selection retention; existing stories do not establish the new assignment-history behavior. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. | +| [Apps, channels, and extensions/External chat and email/Task-linked files](../../ui/storybook/stories/chat-channels/channel-attachments.stories.tsx) | `chat-channels/attachments` | current / production / fixtures | 5 / 2 | TaskChatBubble with received/outbound saved attachment receipts, no/multiple files and missing filename. File links are fictional; no download, provider transfer or authorization is proved. | | [Apps, channels, and extensions/External chat and email/Connections/Chat connection actions](../../ui/storybook/stories/chat-channels/chat-connection-actions.stories.tsx) | `chat-channels/providers` | unreviewed / production / fixtures | 1 / 1 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation reviewed the added connection/destination-default recipe; these unchanged setup, identity or conversation examples do not establish default selection or new-task inheritance. | | [Apps, channels, and extensions/External chat and email/Connections/Chat management](../../ui/storybook/stories/chat-channels/chat-management/chat-management.stories.tsx) | `chat-channels/lifecycle`, `chat-channels/identity`, `chat-channels/conversation` | unreviewed / production / fixtures | 15 / 7 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation: the mounted endpoint adds connection and destination editors. Local endpoint PATCH/resource PUT preserve defaults; workspace-choice requests now return only active/idle, unclosed fixture workspaces. Browser save/error/reload and new-conversation inheritance remain unverified. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Inspected channel task-default controls, optional API fields, avatar rendering and matching fixture updates; live defaults inheritance and permission checks remain outside these stories. Access, InvitePeople, ConnectAndTest and Conversations render the shipped endpoint detail with simulated identity linking and conversation data. External delivery and identity authorization are not live. | | [Apps, channels, and extensions/External chat and email/Connectors/GitHub bot management](../../ui/storybook/stories/chat-channels/github-bot-management/management.stories.tsx) | `chat-channels/providers`, `chat-channels/conversation` | unreviewed / production / fixtures | 33 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Task-workspace reconciliation: mounted repository and endpoint editors use simulated defaults. Endpoint PATCH now persists omitted/null/concrete values across GET and resets on remount; repository PUT already preserves them. Choice caches intentionally contain no projects/workspaces. Browser editor interactions, access denials and new-task inheritance remain unverified. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Inspected channel task-default controls, optional API fields, avatar rendering and matching fixture updates; live defaults inheritance and permission checks remain outside these stories. Codex selection/branding and upstream recipe source changes inspected; prior parity limitations remain, with no new rendered or live proof. | @@ -418,6 +438,7 @@ Tool exposure, client configuration, tokens, profiles, and activity. | Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note | | --- | --- | --- | --- | --- | | [Apps, channels, and extensions/Tool gateways and access profiles/Tools/Access profiles](../../ui/storybook/stories/gateways-profiles/access-profiles.stories.tsx) | `gateways-profiles/profiles` | unreviewed / production / fixtures | 18 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | +| [Apps, channels, and extensions/Tool gateways and access profiles/Named gateway tokens](../../ui/storybook/stories/gateways-profiles/gateway-tokens.stories.tsx) | `gateways-profiles/tokens` | current / production / fixtures | 8 / 5 | Named, empty and expired token history; issuance pending/error/403/success and name-confirmed revocation. Tokens are fictional; real bearer expiry, scope and revocation need API verification. | | [Apps, channels, and extensions/Tool gateways and access profiles/Tools/Gateways (PAP-11182)](../../ui/storybook/stories/gateways-profiles/gateways-tab.stories.tsx) | `gateways-profiles/gateway` | unreviewed / production / fixtures | 3 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Manage connections](../../ui/storybook/stories/gateways-profiles/public-mcp-connections.stories.tsx) | `gateways-profiles/client-connect` | current / production / fixtures | 9 / 4 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Consent](../../ui/storybook/stories/gateways-profiles/public-mcp-consent.stories.tsx) | `gateways-profiles/client-connect` | current / production / fixtures | 25 / 14 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | @@ -436,6 +457,7 @@ Company/user credentials, grants, proposals, and vault import. | [Apps, channels, and extensions/Secrets and proposals/Task credential access/01 Notice](../../ui/storybook/stories/secrets/credential-access/01-notice.stories.tsx) | `secrets/runtime-delivery`, `recovery/diagnosis` | unreviewed / production / fixtures | 5 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Agent-owned instance secrets remain in trusted server runtime and are excluded from ordinary agent delivery; these UI examples do not verify that lifecycle contract. | | [Apps, channels, and extensions/Secrets and proposals/Task credential access/02 Task page](../../ui/storybook/stories/secrets/credential-access/02-task-page.stories.tsx) | `secrets/runtime-delivery`, `recovery/diagnosis` | unreviewed / production / fixtures | 6 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Master reconciliation inspected assignment-history recipient suggestions and manual-selection retention; existing stories do not establish the new assignment-history behavior. Expanded upstream verification recipes were inspected; the existing fixture scenarios do not prove the added backend handoff, workspace, recovery or authorization contracts. Codex selection/branding and upstream recipe source changes inspected; prior parity limitations remain, with no new rendered or live proof. Agent-owned instance secrets remain in trusted server runtime and are excluded from ordinary agent delivery; these UI examples do not verify that lifecycle contract. | | [Apps, channels, and extensions/Secrets and proposals/Product/Environment Variables Editor](../../ui/storybook/stories/secrets/environment-variables-editor.stories.tsx) | `secrets/runtime-delivery` | unreviewed / production / fixtures | 11 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Agent-owned instance secrets remain in trusted server runtime and are excluded from ordinary agent delivery; these UI examples do not verify that lifecycle contract. | +| [Apps, channels, and extensions/Secrets and proposals/Proposal review](../../ui/storybook/stories/secrets/proposal-review.stories.tsx) | `secrets/proposals` | current / production / fixtures | 7 / 3 | Pending proposal, authority preflight block, list states and rejected proposal/rejection error. ProposalsTab and its dialogs are shipped; secret creation, values and grant authority are not exercised. Agent-owned instance secrets remain in trusted server runtime and are excluded from ordinary agent delivery; these UI examples do not verify that lifecycle contract. | | [Apps, channels, and extensions/Secrets and proposals/Product/Secrets](../../ui/storybook/stories/secrets/secrets.stories.tsx) | `secrets/company-secrets`, `secrets/vault-import` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Agent-owned instance secrets remain in trusted server runtime and are excluded from ordinary agent delivery; these UI examples do not verify that lifecycle contract. | | [Apps, channels, and extensions/Secrets and proposals/Product/User secrets](../../ui/storybook/stories/secrets/user-secrets.stories.tsx) | `secrets/user-secrets` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Agent-owned instance secrets remain in trusted server runtime and are excluded from ordinary agent delivery; these UI examples do not verify that lifecycle contract. | @@ -445,8 +467,11 @@ Installation/configuration, contributed pages/tools, and lifecycle. [Feature recipe](../feature-map/plugins.md) -No primary story suite. Check capability lookup below for related examples; this does not imply a missing product feature. - +| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note | +| --- | --- | --- | --- | --- | +| [Apps, channels, and extensions/Plugins/Configuration](../../ui/storybook/stories/plugins/plugin-configuration.stories.tsx) | `plugins/configuration` | current / production / fixtures | 6 / 3 | PluginSettings configuration route with hydrated values, read pending/unavailable and saved/error/403 responses. Real config storage and plugin worker validation are not proved. | +| [Apps, channels, and extensions/Plugins/Contributed routes](../../ui/storybook/stories/plugins/plugin-contributions.stories.tsx) | `plugins/contributions` | current / production / fixtures | 4 / 1 | Production PluginPage host with a registered fixture plugin component; pending/missing contribution and discovery failure. Discovery failure currently remains Loading; contribution contents are a fixture, not shipped plugin code. | +| [Apps, channels, and extensions/Plugins/Installation and lifecycle](../../ui/storybook/stories/plugins/plugin-management.stories.tsx) | `plugins/installation`, `plugins/lifecycle` | current / production / fixtures | 10 / 5 | PluginManager list states, install pending/error/success, disable and uninstall receipts. Local record transitions only; package loading and worker lifecycle require production verification. | ## Automation and structured work @@ -463,6 +488,7 @@ Definitions, variables, scheduled/webhook/manual runs, and history. | [Automation and structured work/Routines, schedules, and triggers/Product/Routines/Webhooks UX preview](../../ui/storybook/stories/routines/routine-webhooks-ux.stories.tsx) | `routines/triggers` | unreviewed / production / fixtures | 21 / 5 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Automation and structured work/Routines, schedules, and triggers/Product/Routines/Webhooks](../../ui/storybook/stories/routines/routine-webhooks.stories.tsx) | `routines/triggers`, `routines/history`, `activity/routine-audit` | unreviewed / production / fixtures | 16 / 14 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Runs and Activity mount RoutineDetail with fixture execution tasks/events and play assertions. Scheduler execution, delivery and audit persistence are not live. | | [Automation and structured work/Routines, schedules, and triggers/Product/Routines · List (grouped cards)](../../ui/storybook/stories/routines/routines-list-groups.stories.tsx) | `routines/definition`, `routines/management` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | +| [Automation and structured work/Routines, schedules, and triggers/Run now](../../ui/storybook/stories/routines/run-now.stories.tsx) | `routines/run-now` | current / production / fixtures | 8 / 4 | RoutineDetail manual-run dialog with required input, captured variables, pending/error/success and read states. Fictional dispatch receipt; real scheduling, coalescing, workspace authority and execution remain separate. | ### Pipelines, review queues, and learnings @@ -470,8 +496,12 @@ Experimental stages, automation, items, review, and learning records. [Feature recipe](../feature-map/pipelines.md) -No primary story suite. Check capability lookup below for related examples; this does not imply a missing product feature. - +| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note | +| --- | --- | --- | --- | --- | +| [Automation and structured work/Pipelines, review queues, and learnings/Learnings](../../ui/storybook/stories/pipelines/learnings.stories.tsx) | `pipelines/learnings` | current / production / fixtures | 5 / 1 | Production Learnings feed renders a review_decided event plus loading/empty/error/403. Event provenance and durable audit records remain separate; this is not a new learning-record subsystem. | +| [Automation and structured work/Pipelines, review queues, and learnings/Items](../../ui/storybook/stories/pipelines/pipeline-board.stories.tsx) | `pipelines/items` | current / production / fixtures | 5 / 1 | Production Pipelines board with stages/items and loading/empty/error/403. Snapshot rows only; transitions, liveness, guards and automation execution remain separate. | +| [Automation and structured work/Pipelines, review queues, and learnings/Review queue](../../ui/storybook/stories/pipelines/review-queue.stories.tsx) | `pipelines/review` | current / production / fixtures | 7 / 3 | Production ReviewQueue with empty/loading/error/403, approval removal and failure recovery marked Needs attention. Fixture decisions do not prove real review leases, version checks or transitions. | +| [Automation and structured work/Pipelines, review queues, and learnings/Stage settings](../../ui/storybook/stories/pipelines/stage-settings.stories.tsx) | `pipelines/stages` | current / production / fixtures | 5 / 2 | Production PipelineSettings stage editor with filled configuration, no stages, pending/error/403. This pass renders the shipped controls; saved stage/automation changes remain production obligations. | ### Cases @@ -479,8 +509,10 @@ Experimental structured fields, relationships, revisions, and task links. [Feature recipe](../feature-map/cases.md) -No primary story suite. Check capability lookup below for related examples; this does not imply a missing product feature. - +| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note | +| --- | --- | --- | --- | --- | +| [Automation and structured work/Cases/Browse and hierarchy](../../ui/storybook/stories/cases/case-browse.stories.tsx) | `cases/browse` | current / production / fixtures | 5 / 1 | Cases page with fixture parent/child records and loading/empty/error/403. Failed list queries currently use the shipped empty-case presentation. Responsive duplicate labels are accounted for; server filters and case mutations remain separate. | +| [Automation and structured work/Cases/Structure, history and task links](../../ui/storybook/stories/cases/case-detail.stories.tsx) | `cases/structure`, `cases/history`, `cases/task-links` | current / production / fixtures | 8 / 4 | CaseDetail hierarchy and activity, CaseFieldsPanel arbitrary/empty data and IssueCasesPanel reverse links. Fixture relations/events; document revision history and persisted links remain production obligations. | ### Status cards @@ -488,8 +520,9 @@ Experimental summaries, watched work, refresh history, and settings. [Feature recipe](../feature-map/status-cards.md) -No primary story suite. Check capability lookup below for related examples; this does not imply a missing product feature. - +| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note | +| --- | --- | --- | --- | --- | +| [Automation and structured work/Status cards/Authoring, updates and archive](../../ui/storybook/stories/status-cards/status-card-states.stories.tsx) | `status-cards/authoring`, `status-cards/generation`, `status-cards/inspection`, `status-cards/archive` | current / production / fixtures | 17 / 10 | StatusCards board/drawer with current/stale/error/live setup, list states, create, refresh, inspection and archive/restoration. Mutable receipts and setup task ID are fixtures; no summarizer execution or revision-history parity is claimed. | ## Oversight and operation @@ -502,6 +535,7 @@ Spend reports, scoped limits, incidents, hard stops, and resumption. | Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note | | --- | --- | --- | --- | --- | | [Oversight and operation/Costs and budgets/Product/Budget & Finance](../../ui/storybook/stories/budgets-costs/budget-finance.stories.tsx) | `budgets-costs/cost-reporting`, `budgets-costs/budgets`, `budgets-costs/incidents` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. FullMatrix renders BudgetIncidentCard in warning, resolved, pending-approval and escalated hard-stop states. Resolution handlers are no-ops; budget relief and resumption remain unverified. The upstream budget UI distinguishes soft warnings from hard stops; hard incidents retain raise-and-resume and keep-paused controls. | +| [Oversight and operation/Costs and budgets/Resolve a budget incident](../../ui/storybook/stories/budgets-costs/budget-resolution.stories.tsx) | `budgets-costs/resolution` | current / production / fixtures | 10 / 6 | Shipped Costs budget tab with empty/read failures, invalid amount, raise-and-resume, keep-paused and write failure/pending. Fixture incident transitions only; real overlapping holds and hard stops remain separate. The upstream budget UI distinguishes soft warnings from hard stops; hard incidents retain raise-and-resume and keep-paused controls. | | [Oversight and operation/Costs and budgets/Costs/Subscriptions](../../ui/storybook/stories/budgets-costs/costs/subscriptions.stories.tsx) | `budgets-costs/cost-reporting`, `budgets-costs/budgets` | unreviewed / production / fixtures | 7 / 4 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. The upstream budget UI distinguishes soft warnings from hard stops; hard incidents retain raise-and-resume and keep-paused controls. | ### Dashboards and audit trails @@ -515,6 +549,7 @@ Company health, live work, activity, runs, routines, and timeline. | [Oversight and operation/Dashboards and audit trails/Tools/Activity (audit feed)](../../ui/storybook/stories/activity/activity-feed.stories.tsx) | `activity/activity`, `gateways-profiles/activity` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Oversight and operation/Dashboards and audit trails/Apps/App detail · Activity (lifecycle)](../../ui/storybook/stories/activity/app-activity-lifecycle.stories.tsx) | `activity/activity`, `gateways-profiles/activity` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Oversight and operation/Dashboards and audit trails/Product/Data Visualization & Misc](../../ui/storybook/stories/activity/data-viz-misc.stories.tsx) | `activity/overview` | unreviewed / production / fixtures | 21 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. Codex selection/branding and upstream recipe source changes inspected; prior parity limitations remain, with no new rendered or live proof. | +| [Oversight and operation/Dashboards and audit trails/Execution history](../../ui/storybook/stories/activity/run-audit.stories.tsx) | `activity/runs` | current / production / fixtures | 6 / 4 | AuditRuns list, completed receipt, empty/loading/failed/denied and no-match filter. Run records are fixture snapshots; no real execution or scoped audit proof. | | [Oversight and operation/Dashboards and audit trails/Pages/Work Timeline](../../ui/storybook/stories/activity/work-timeline.stories.tsx) | `activity/timeline` | unreviewed / production / fixtures | 4 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | ### Navigation, profile, and announcements @@ -528,6 +563,7 @@ Sidebar state, favorites/recents, personal identity, and dismissals. | [Oversight and operation/Navigation, profile, and announcements/Announcements/AnnouncementCard](../../ui/storybook/stories/navigation-preferences/announcement-card.stories.tsx) | `navigation-preferences/announcements` | unreviewed / production / fixtures | 11 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | | [Oversight and operation/Navigation, profile, and announcements/Prototypes/Task detail/Mobile tabs](../../ui/storybook/stories/navigation-preferences/mobile-task-tabs.stories.tsx) | `navigation-preferences/navigation` | proposed / prototype / fixtures | 4 / 2 | Local composition retained for its original design purpose; full production parity remains unreviewed. | | [Oversight and operation/Navigation, profile, and announcements/Product/Navigation & Layout](../../ui/storybook/stories/navigation-preferences/navigation-layout.stories.tsx) | `navigation-preferences/navigation` | unreviewed / production / fixtures | 2 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. | +| [Oversight and operation/Navigation, profile, and announcements/Sidebar preferences](../../ui/storybook/stories/navigation-preferences/sidebar-preferences.stories.tsx) | `navigation-preferences/preferences` | current / production / fixtures | 5 / 1 | Production Sidebar inside PluginLauncherProvider with saved-order reads, pending/error/403 fallback and session-only section collapse. Favorites, recents and actual order persistence still need the recipe checks. | ### Instance operations @@ -537,6 +573,7 @@ Installation, updates, service health, configuration, and backups. | Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note | | --- | --- | --- | --- | --- | +| [Oversight and operation/Instance operations/Backup retention](../../ui/storybook/stories/instance-operations/backup-retention.stories.tsx) | `instance-operations/backup`, `instance-operations/configuration` | current / production / fixtures | 6 / 6 | Retention presets, initial read, loading, error, denial and pending/saved selection in InstanceGeneralSettings. Backup creation/restore and real settings authority remain production obligations. The recipe distinguishes process liveness from authenticated database readiness; these examples do not exercise that operator API boundary. The Codex branch relocates the liveness/readiness recipe paragraph to operator verification without changing its contract; this suite retains separate backup/restore and live readiness obligations. | | [Oversight and operation/Instance operations/Product/Dev Ops Surfaces](../../ui/storybook/stories/instance-operations/dev-ops-surfaces.stories.tsx) | `instance-operations/server-service` | unreviewed / production / fixtures | 1 / 0 | Shipped subjects identified through imports and local wrappers; full state, fixture-contract and route parity remain unreviewed. The recipe distinguishes process liveness from authenticated database readiness; these examples do not exercise that operator API boundary. | ### CLI/API and local worktrees @@ -582,18 +619,18 @@ References are navigation links, not proof of parity. An absent link means no ca - `onboarding/company`: [Getting started and access/Onboarding and first work/Onboarding/Character](../../ui/storybook/stories/onboarding/onboarding-character.stories.tsx) (unreviewed) - `onboarding/adapter`: [Getting started and access/Onboarding and first work/Onboarding/Agent arc](../../ui/storybook/stories/onboarding/onboarding-agent-arc.stories.tsx) (unreviewed), [Getting started and access/Onboarding and first work/Onboarding/Connect a model](../../ui/storybook/stories/onboarding/onboarding-connect-model.stories.tsx) (historical) - `onboarding/first-task`: [Getting started and access/Onboarding and first work/Onboarding/Agent arc](../../ui/storybook/stories/onboarding/onboarding-agent-arc.stories.tsx) (unreviewed), [Getting started and access/Onboarding and first work/Onboarding/Connect a model](../../ui/storybook/stories/onboarding/onboarding-connect-model.stories.tsx) (historical) -- `onboarding/resume`: No cataloged story. -- `access/login`: No cataloged story. +- `onboarding/resume`: [Getting started and access/Onboarding and first work/Resume a saved draft](../../ui/storybook/stories/onboarding/resume-draft.stories.tsx) (current) +- `access/login`: [Getting started and access/Login, invitations, and access/Sign in](../../ui/storybook/stories/access/sign-in.stories.tsx) (current) - `access/invitations`: [Agents and reusable capabilities/Hiring, configuration, and organization/Onboarding/External agent invitation/Components](../../ui/storybook/stories/agents/external-agent-invite/components.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Onboarding/External agent invitation/Journeys](../../ui/storybook/stories/agents/external-agent-invite/journeys.stories.tsx) (unreviewed) -- `access/membership`: No cataloged story. +- `access/membership`: [Getting started and access/Login, invitations, and access/Organization membership](../../ui/storybook/stories/access/membership.stories.tsx) (current) - `access/resources`: [Getting started and access/Login, invitations, and access/Private tasks/00 Review guide](../../ui/storybook/stories/access/private-tasks/00-review-guide.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/01 Creation](../../ui/storybook/stories/access/private-tasks/01-creation.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/02 Sharing](../../ui/storybook/stories/access/private-tasks/02-sharing.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/03 Task actions](../../ui/storybook/stories/access/private-tasks/03-task-actions.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/04 References and blockers](../../ui/storybook/stories/access/private-tasks/04-references-and-blockers.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/05 Projects](../../ui/storybook/stories/access/private-tasks/05-projects.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/06 Full product pages](../../ui/storybook/stories/access/private-tasks/06-full-product-pages.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/07 User stories](../../ui/storybook/stories/access/private-tasks/07-user-stories.stories.tsx) (unreviewed) -- `access/cli-auth`: No cataloged story. -- `companies/selection`: No cataloged story. +- `access/cli-auth`: [Getting started and access/Login, invitations, and access/CLI authorization](../../ui/storybook/stories/access/cli-authorization.stories.tsx) (current) +- `companies/selection`: [Getting started and access/Companies and portability/Selection and archived organizations](../../ui/storybook/stories/companies/company-selection.stories.tsx) (current) - `companies/identity`: [Getting started and access/Companies and portability/Decision models/01 Settings](../../ui/storybook/stories/companies/decision-models/01-settings.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Decision models/02 History](../../ui/storybook/stories/companies/decision-models/02-history.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Decision models/03 Journey](../../ui/storybook/stories/companies/decision-models/03-journey.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Fast responses/01 Settings](../../ui/storybook/stories/companies/fast-responses/01-settings.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Fast responses/02 History](../../ui/storybook/stories/companies/fast-responses/02-history.stories.tsx) (current) - `companies/decision-models`: [Getting started and access/Companies and portability/Decision models/01 Settings](../../ui/storybook/stories/companies/decision-models/01-settings.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Decision models/02 History](../../ui/storybook/stories/companies/decision-models/02-history.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Decision models/03 Journey](../../ui/storybook/stories/companies/decision-models/03-journey.stories.tsx) (unreviewed) - `companies/fast-responses`: [Getting started and access/Companies and portability/Fast responses/01 Settings](../../ui/storybook/stories/companies/fast-responses/01-settings.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Fast responses/02 History](../../ui/storybook/stories/companies/fast-responses/02-history.stories.tsx) (current) -- `companies/archive`: No cataloged story. -- `companies/portability`: No cataloged story. +- `companies/archive`: [Getting started and access/Companies and portability/Selection and archived organizations](../../ui/storybook/stories/companies/company-selection.stories.tsx) (current) +- `companies/portability`: [Getting started and access/Companies and portability/Import preview](../../ui/storybook/stories/companies/company-import.stories.tsx) (current) - `tasks/create`: [Tasks and conversations/Task creation and lifecycle/Composer/Add menu](../../ui/storybook/stories/tasks/composer-add-menu.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Composer/Mobile assignee picker](../../ui/storybook/stories/tasks/composer-mobile-assignee.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Composer/Model and effort picker](../../ui/storybook/stories/tasks/composer-model-picker.stories.tsx) (mixed), [Tasks and conversations/Task creation and lifecycle/Product/Dialogs & Modals](../../ui/storybook/stories/tasks/dialogs-modals.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Components/Entity pickers/Mobile](../../ui/storybook/stories/tasks/mobile-entity-pickers.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Composer/New task](../../ui/storybook/stories/tasks/new-task-composer.stories.tsx) (current), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/00 Review guide](../../ui/storybook/stories/agents/primary-agent/00-review-guide.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/01 Components](../../ui/storybook/stories/agents/primary-agent/01-components.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/02 Sidebar](../../ui/storybook/stories/agents/primary-agent/02-sidebar.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/03 Agents](../../ui/storybook/stories/agents/primary-agent/03-agents.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/04 New task](../../ui/storybook/stories/agents/primary-agent/04-new-task.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/05 Chat](../../ui/storybook/stories/agents/primary-agent/05-chat.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/06 Journeys](../../ui/storybook/stories/agents/primary-agent/06-journeys.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/07 Confirmation](../../ui/storybook/stories/agents/primary-agent/07-confirmation.stories.tsx) (unreviewed) - `tasks/organize`: [Tasks and conversations/Task creation and lifecycle/Product/Control Plane Surfaces](../../ui/storybook/stories/tasks/control-plane-surfaces.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Product/Issue Management](../../ui/storybook/stories/tasks/issue-management.stories.tsx) (unreviewed) - `tasks/properties`: [Tasks and conversations/Task creation and lifecycle/External Objects](../../ui/storybook/stories/tasks/external-objects.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Product/Issue Management](../../ui/storybook/stories/tasks/issue-management.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/00 Review guide](../../ui/storybook/stories/access/private-tasks/00-review-guide.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/01 Creation](../../ui/storybook/stories/access/private-tasks/01-creation.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/02 Sharing](../../ui/storybook/stories/access/private-tasks/02-sharing.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/03 Task actions](../../ui/storybook/stories/access/private-tasks/03-task-actions.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/04 References and blockers](../../ui/storybook/stories/access/private-tasks/04-references-and-blockers.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/05 Projects](../../ui/storybook/stories/access/private-tasks/05-projects.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/06 Full product pages](../../ui/storybook/stories/access/private-tasks/06-full-product-pages.stories.tsx) (unreviewed), [Getting started and access/Login, invitations, and access/Private tasks/07 User stories](../../ui/storybook/stories/access/private-tasks/07-user-stories.stories.tsx) (unreviewed) @@ -603,15 +640,15 @@ References are navigation links, not proof of parity. An absent link means no ca - `task-coordination/dependencies`: [Projects and execution/Recovery/Product/Issue/Blocked notice](../../ui/storybook/stories/recovery/issue-blocked-notice.stories.tsx) (unreviewed), [Tasks and conversations/Delegation, dependencies, and signoff/UX Labs/Sub-issues Workflow Checklist](../../ui/storybook/stories/task-coordination/sub-issues-workflow.stories.tsx) (unreviewed) - `task-coordination/signoff`: [Tasks and conversations/Delegation, dependencies, and signoff/Product/Decisions/Stalled review actions](../../ui/storybook/stories/task-coordination/issue-review-panel.stories.tsx) (unreviewed) - `task-coordination/tree-control`: [Tasks and conversations/Delegation, dependencies, and signoff/Product/Issue Monitor surfaces](../../ui/storybook/stories/task-coordination/monitor-surfaces.stories.tsx) (unreviewed), [Tasks and conversations/Delegation, dependencies, and signoff/Product/Task Watchdog surfaces](../../ui/storybook/stories/task-coordination/task-watchdog-surfaces.stories.tsx) (unreviewed) -- `inbox-search/views`: No cataloged story. +- `inbox-search/views`: [Tasks and conversations/Inbox, decisions, and search/Personal task views](../../ui/storybook/stories/inbox-search/inbox-views.stories.tsx) (current) - `inbox-search/triage`: [Tasks and conversations/Inbox, decisions, and search/Product/Inbox/Blocked tab](../../ui/storybook/stories/inbox-search/blocked-inbox.stories.tsx) (unreviewed) - `inbox-search/decisions`: [Tasks and conversations/Inbox, decisions, and search/Decisions/DecisionCard](../../ui/storybook/stories/inbox-search/decision-card.stories.tsx) (unreviewed), [Tasks and conversations/Inbox, decisions, and search/Pages/Decisions Desk](../../ui/storybook/stories/inbox-search/decisions-desk.stories.tsx) (unreviewed), [Tasks and conversations/Inbox, decisions, and search/Pages/Decisions](../../ui/storybook/stories/inbox-search/what-needs-me.stories.tsx) (unreviewed) - `inbox-search/search`: [Tasks and conversations/Inbox, decisions, and search/Product/Search & Command K](../../ui/storybook/stories/inbox-search/search.stories.tsx) (unreviewed) -- `inbox-search/toasts`: No cataloged story. +- `inbox-search/toasts`: [Tasks and conversations/Inbox, decisions, and search/Personal task notifications](../../ui/storybook/stories/inbox-search/personal-task-toasts.stories.tsx) (current) - `agent-chat/discovery`: [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat sidebar/Pages](../../ui/storybook/stories/agent-chat/agent-chat-sidebar.stories.tsx) (unreviewed), [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat](../../ui/storybook/stories/agent-chat/agent-chat.stories.tsx) (mixed), [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat sidebar/Components](../../ui/storybook/stories/agent-chat/agent-conversation-sidebar.stories.tsx) (current), [Tasks and conversations/Agent conversations and project handoff/Design explorations/Chat entry](../../ui/storybook/stories/agent-chat/chat-entry.stories.tsx) (historical), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/00 Review guide](../../ui/storybook/stories/agents/primary-agent/00-review-guide.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/01 Components](../../ui/storybook/stories/agents/primary-agent/01-components.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/02 Sidebar](../../ui/storybook/stories/agents/primary-agent/02-sidebar.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/03 Agents](../../ui/storybook/stories/agents/primary-agent/03-agents.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/04 New task](../../ui/storybook/stories/agents/primary-agent/04-new-task.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/05 Chat](../../ui/storybook/stories/agents/primary-agent/05-chat.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/06 Journeys](../../ui/storybook/stories/agents/primary-agent/06-journeys.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Primary Agent/07 Confirmation](../../ui/storybook/stories/agents/primary-agent/07-confirmation.stories.tsx) (unreviewed) - `agent-chat/history`: [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat sidebar/Pages](../../ui/storybook/stories/agent-chat/agent-chat-sidebar.stories.tsx) (unreviewed), [Tasks and conversations/Questions and approvals/Chat & Comments/Agent Chat Unanswered Questions](../../ui/storybook/stories/questions-and-approvals/agent-chat-unanswered-questions.stories.tsx) (unreviewed), [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat](../../ui/storybook/stories/agent-chat/agent-chat.stories.tsx) (mixed), [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat sidebar/Components](../../ui/storybook/stories/agent-chat/agent-conversation-sidebar.stories.tsx) (current), [Tasks and conversations/Agent conversations and project handoff/Design explorations/Chat entry](../../ui/storybook/stories/agent-chat/chat-entry.stories.tsx) (historical) - `agent-chat/handoff`: [Tasks and conversations/Agent conversations and project handoff/Design explorations/Agent chat](../../ui/storybook/stories/agent-chat/agent-chat.stories.tsx) (mixed) -- `agent-chat/conference-room`: No cataloged story. +- `agent-chat/conference-room`: [Tasks and conversations/Agent conversations and project handoff/Conference Room](../../ui/storybook/stories/agent-chat/conference-room.stories.tsx) (current) - `questions-and-approvals/questions`: [Tasks and conversations/Questions and approvals/Composer/Question flow](../../ui/storybook/stories/questions-and-approvals/composer-question-flow.stories.tsx) (unreviewed), [Tasks and conversations/Questions and approvals/Chat & Comments/Issue Thread Interactions](../../ui/storybook/stories/questions-and-approvals/issue-thread-interactions.stories.tsx) (unreviewed), [Tasks and conversations/Questions and approvals/Chat & Comments/Rich ACP interactions](../../ui/storybook/stories/questions-and-approvals/rich-acp-interactions.stories.tsx) (unreviewed), [Tasks and conversations/Questions and approvals/Composer/Interaction above composer](../../ui/storybook/stories/questions-and-approvals/task-chat-interaction-above-composer.stories.tsx) (current) - `questions-and-approvals/dismiss-and-reopen`: [Tasks and conversations/Questions and approvals/Chat & Comments/Agent Chat Unanswered Questions](../../ui/storybook/stories/questions-and-approvals/agent-chat-unanswered-questions.stories.tsx) (unreviewed), [Tasks and conversations/Questions and approvals/Chat & Comments/Task Chat Unanswered Questions](../../ui/storybook/stories/questions-and-approvals/task-chat-unanswered-questions.stories.tsx) (unreviewed) - `questions-and-approvals/confirmations`: [Tasks and conversations/Questions and approvals/Chat & Comments/Issue Thread Interactions](../../ui/storybook/stories/questions-and-approvals/issue-thread-interactions.stories.tsx) (unreviewed), [Tasks and conversations/Questions and approvals/Chat & Comments/Rich ACP interactions](../../ui/storybook/stories/questions-and-approvals/rich-acp-interactions.stories.tsx) (unreviewed), [Tasks and conversations/Questions and approvals/Composer/Interaction above composer](../../ui/storybook/stories/questions-and-approvals/task-chat-interaction-above-composer.stories.tsx) (current) @@ -625,9 +662,9 @@ References are navigation links, not proof of parity. An absent link means no ca - `steering/steer`: [Tasks and conversations/Steering and queued messages/Composer/Steering and queued messages](../../ui/storybook/stories/steering/composer-queued-messages.stories.tsx) (current) - `steering/interrupt`: [Tasks and conversations/Steering and queued messages/Composer/Steering and queued messages](../../ui/storybook/stories/steering/composer-queued-messages.stories.tsx) (current), [Tasks and conversations/Steering and queued messages/Surfaces/Interrupt Handoff](../../ui/storybook/stories/steering/interrupt-handoff.stories.tsx) (unreviewed) - `steering/stop-and-pause`: [Tasks and conversations/Steering and queued messages/Composer/Paused task takeover](../../ui/storybook/stories/steering/paused-composer.stories.tsx) (historical), [Tasks and conversations/Steering and queued messages/Composer/Execution controls](../../ui/storybook/stories/steering/task-execution-controls.stories.tsx) (current) -- `steering/drafts`: No cataloged story. -- `steering/approval-queue`: No cataloged story. -- `documents-artifacts/documents`: No cataloged story. +- `steering/drafts`: [Tasks and conversations/Steering and queued messages/Draft recovery](../../ui/storybook/stories/steering/composer-draft-recovery.stories.tsx) (current) +- `steering/approval-queue`: [Tasks and conversations/Steering and queued messages/Accepted response delivery](../../ui/storybook/stories/steering/durable-response-queue.stories.tsx) (current) +- `documents-artifacts/documents`: [Tasks and conversations/Documents, attachments, and work products/Document revisions](../../ui/storybook/stories/documents-artifacts/document-revisions.stories.tsx) (current) - `documents-artifacts/annotations`: [Tasks and conversations/Documents, attachments, and work products/Product/Documents/Annotations](../../ui/storybook/stories/documents-artifacts/document-annotations.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Product/Document Comments](../../ui/storybook/stories/documents-artifacts/document-comments.stories.tsx) (unreviewed) - `documents-artifacts/attachments`: [Tasks and conversations/Documents, attachments, and work products/Artifact upload/01 Artifacts pane](../../ui/storybook/stories/documents-artifacts/artifact-upload/01-components.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Artifact upload/02 Paused task journey](../../ui/storybook/stories/documents-artifacts/artifact-upload/02-task-journey.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Components/CSV File Tab](../../ui/storybook/stories/documents-artifacts/csv-preview.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Components/File Viewer Sheet](../../ui/storybook/stories/documents-artifacts/file-viewer.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/01 Preview](../../ui/storybook/stories/documents-artifacts/html-artifacts/01-preview.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/02 Workspace](../../ui/storybook/stories/documents-artifacts/html-artifacts/02-workspace.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/03 Task journey](../../ui/storybook/stories/documents-artifacts/html-artifacts/03-task-journey.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Tasks/Text file tabs](../../ui/storybook/stories/documents-artifacts/task-text-files.stories.tsx) (unreviewed) - `documents-artifacts/work-products`: [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Commit](../../ui/storybook/stories/documents-artifacts/artifact-cards/commit.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Data](../../ui/storybook/stories/documents-artifacts/artifact-cards/data.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Document](../../ui/storybook/stories/documents-artifacts/artifact-cards/document.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/File](../../ui/storybook/stories/documents-artifacts/artifact-cards/file.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Image](../../ui/storybook/stories/documents-artifacts/artifact-cards/image.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Link preview](../../ui/storybook/stories/documents-artifacts/artifact-cards/link-preview.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Pull request](../../ui/storybook/stories/documents-artifacts/artifact-cards/pull-request.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Explorations/Artifact cards/Video](../../ui/storybook/stories/documents-artifacts/artifact-cards/video.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Pages/Artifacts](../../ui/storybook/stories/documents-artifacts/artifacts.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Task Chat/Rich Work Product Cards](../../ui/storybook/stories/documents-artifacts/rich-work-product-cards.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/Tasks/Artifact Gallery](../../ui/storybook/stories/documents-artifacts/task-artifact-gallery.stories.tsx) (unreviewed) @@ -646,7 +683,7 @@ References are navigation links, not proof of parity. An absent link means no ca - `skills/authoring`: [Agents and reusable capabilities/Skills and Skill Studio/Skill Studio/EditACopy](../../ui/storybook/stories/skills/fork-skill-flow.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Product/Skill Studio · FrontmatterPanel](../../ui/storybook/stories/skills/frontmatter-panel.stories.tsx) (unreviewed) - `skills/versions`: [Agents and reusable capabilities/Skills and Skill Studio/Skills/GitHub sync journey](../../ui/storybook/stories/skills/github-skills-journey.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Skills/GitHub synced skills](../../ui/storybook/stories/skills/github-synced-skills.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Skills/Repository packages](../../ui/storybook/stories/skills/repository-skill-packages/repository-skill-packages.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Skills/Import progress](../../ui/storybook/stories/skills/skill-import-progress.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Skills Store/Skill detail](../../ui/storybook/stories/skills/skills-store-detail.stories.tsx) (unreviewed) - `skills/policy`: [Agents and reusable capabilities/Skills and Skill Studio/Agent Skills/AgentSkillRow](../../ui/storybook/stories/skills/agent-skill-row.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/QA / Agent skills edge states](../../ui/storybook/stories/skills/agent-skills-edge.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Skill Studio/AgentsUsingSkill](../../ui/storybook/stories/skills/agents-using-skill.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Skills/GitHub sync journey](../../ui/storybook/stories/skills/github-skills-journey.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Product/Skills/Policy surfaces](../../ui/storybook/stories/skills/skill-policy-surfaces.stories.tsx) (unreviewed), [Agents and reusable capabilities/Skills and Skill Studio/Skills Store/Skill detail](../../ui/storybook/stories/skills/skills-store-detail.stories.tsx) (unreviewed) -- `skills/testing`: No cataloged story. +- `skills/testing`: [Agents and reusable capabilities/Skills and Skill Studio/Test runs](../../ui/storybook/stories/skills/skill-test-runs.stories.tsx) (current) - `teams/discovery`: [Agents and reusable capabilities/Team packages and installation/Surfaces/Team Catalog](../../ui/storybook/stories/teams/team-catalog.stories.tsx) (unreviewed) - `teams/preview`: [Agents and reusable capabilities/Team packages and installation/Surfaces/Team Catalog](../../ui/storybook/stories/teams/team-catalog.stories.tsx) (unreviewed) - `teams/install`: [Agents and reusable capabilities/Team packages and installation/Surfaces/Team Catalog](../../ui/storybook/stories/teams/team-catalog.stories.tsx) (unreviewed) @@ -656,25 +693,25 @@ References are navigation links, not proof of parity. An absent link means no ca - `projects/repositories`: [Projects and execution/Projects and repositories/Proposals/Project repos/New project](../../ui/storybook/stories/projects/new-project-repos.stories.tsx) (proposed), [Projects and execution/Projects and repositories/Proposals/Project repos/Configuration](../../ui/storybook/stories/projects/project-repos-configuration.stories.tsx) (proposed) - `projects/defaults`: [Projects and execution/Projects and repositories/Workspaces/Project runtime provision command](../../ui/storybook/stories/projects/project-execution-workspace-strategy.stories.tsx) (unreviewed) - `goals/hierarchy`: [Tasks and conversations/Task creation and lifecycle/Product/Dialogs & Modals](../../ui/storybook/stories/tasks/dialogs-modals.stories.tsx) (unreviewed), [Projects and execution/Projects and repositories/Product/Projects Goals Workspaces](../../ui/storybook/stories/projects/projects-goals-workspaces.stories.tsx) (unreviewed) -- `goals/alignment`: No cataloged story. -- `goals/navigation`: No cataloged story. +- `goals/alignment`: [Projects and execution/Goals and work alignment/Aligned work and navigation](../../ui/storybook/stories/goals/goal-alignment.stories.tsx) (current) +- `goals/navigation`: [Projects and execution/Goals and work alignment/Aligned work and navigation](../../ui/storybook/stories/goals/goal-alignment.stories.tsx) (current) - `workspaces/binding`: [Tasks and conversations/Task creation and lifecycle/Composer/New task](../../ui/storybook/stories/tasks/new-task-composer.stories.tsx) (current), [Projects and execution/Projects and repositories/Product/Projects Goals Workspaces](../../ui/storybook/stories/projects/projects-goals-workspaces.stories.tsx) (unreviewed), [Projects and execution/Workspaces, services, and files/Components/SearchableSelect/Workspace picker](../../ui/storybook/stories/workspaces/searchable-select.stories.tsx) (unreviewed) - `workspaces/provisioning`: [Projects and execution/Projects and repositories/Workspaces/Project runtime provision command](../../ui/storybook/stories/projects/project-execution-workspace-strategy.stories.tsx) (unreviewed), [Projects and execution/Workspaces, services, and files/Workspaces/Runtime provisioning status](../../ui/storybook/stories/workspaces/workspace-runtime-provisioning.stories.tsx) (unreviewed) - `workspaces/services`: [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Task activity](../../ui/storybook/stories/workspaces/browser-use-activity.stories.tsx) (unreviewed), [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Footer](../../ui/storybook/stories/workspaces/browser-use-footer.stories.tsx) (unreviewed), [Projects and execution/Workspaces, services, and files/Tasks/Browser Use/Panel](../../ui/storybook/stories/workspaces/browser-use.stories.tsx) (unreviewed), [Projects and execution/Workspaces, services, and files/Workspaces/Service control bar](../../ui/storybook/stories/workspaces/workspace-service-control-bar.stories.tsx) (unreviewed) - `workspaces/files-git`: [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/01 Preview](../../ui/storybook/stories/documents-artifacts/html-artifacts/01-preview.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/02 Workspace](../../ui/storybook/stories/documents-artifacts/html-artifacts/02-workspace.stories.tsx) (unreviewed), [Tasks and conversations/Documents, attachments, and work products/HTML artifacts/03 Task journey](../../ui/storybook/stories/documents-artifacts/html-artifacts/03-task-journey.stories.tsx) (unreviewed), [Projects and execution/Workspaces, services, and files/Issue/Workspace File Browser](../../ui/storybook/stories/workspaces/workspace-file-browser.stories.tsx) (unreviewed) -- `workspaces/closure`: No cataloged story. -- `execution-environments/configuration`: No cataloged story. -- `execution-environments/capabilities`: No cataloged story. -- `execution-environments/probe`: No cataloged story. +- `workspaces/closure`: [Projects and execution/Workspaces, services, and files/Close readiness](../../ui/storybook/stories/workspaces/workspace-close.stories.tsx) (current) +- `execution-environments/configuration`: [Projects and execution/Execution environments/Configuration, capabilities, and probes](../../ui/storybook/stories/execution-environments/environment-states.stories.tsx) (current) +- `execution-environments/capabilities`: [Projects and execution/Execution environments/Configuration, capabilities, and probes](../../ui/storybook/stories/execution-environments/environment-states.stories.tsx) (current) +- `execution-environments/probe`: [Projects and execution/Execution environments/Configuration, capabilities, and probes](../../ui/storybook/stories/execution-environments/environment-states.stories.tsx) (current) - `execution-environments/assignment`: [Agents and reusable capabilities/Hiring, configuration, and organization/Product/Agent Management](../../ui/storybook/stories/agents/agent-management.stories.tsx) (unreviewed) -- `execution-environments/images`: No cataloged story. +- `execution-environments/images`: [Projects and execution/Execution environments/Custom images](../../ui/storybook/stories/execution-environments/custom-images.stories.tsx) (current) - `recovery/diagnosis`: [Tasks and conversations/Task creation and lifecycle/Paperclip/Assigned Backlog Safeguards](../../ui/storybook/stories/tasks/assigned-backlog-safeguards.stories.tsx) (unreviewed), [Tasks and conversations/Inbox, decisions, and search/Product/Inbox/Blocked tab](../../ui/storybook/stories/inbox-search/blocked-inbox.stories.tsx) (unreviewed), [Apps, channels, and extensions/Secrets and proposals/Task credential access/01 Notice](../../ui/storybook/stories/secrets/credential-access/01-notice.stories.tsx) (unreviewed), [Apps, channels, and extensions/Secrets and proposals/Task credential access/02 Task page](../../ui/storybook/stories/secrets/credential-access/02-task-page.stories.tsx) (unreviewed), [Projects and execution/Recovery/Task chat/Recovery notice](../../ui/storybook/stories/recovery/execution-blocker-notice.stories.tsx) (unreviewed), [Projects and execution/Recovery/Tasks/Execution recovery](../../ui/storybook/stories/recovery/execution-recovery.stories.tsx) (unreviewed), [Projects and execution/Recovery/Product/Issue/Blocked notice](../../ui/storybook/stories/recovery/issue-blocked-notice.stories.tsx) (unreviewed), [Projects and execution/Recovery/Design previews/Recovery notice](../../ui/storybook/stories/recovery/recovery-notice-preview.stories.tsx) (current), [Projects and execution/Recovery/Paperclip/Source Issue Recovery](../../ui/storybook/stories/recovery/source-issue-recovery.stories.tsx) (unreviewed), [Projects and execution/Recovery/Paperclip/Source-resolved Fold](../../ui/storybook/stories/recovery/source-resolved-fold.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Paperclip/Successful Run Handoff](../../ui/storybook/stories/tasks/successful-run-handoff.stories.tsx) (unreviewed) - `recovery/bounded-continuation`: [Projects and execution/Recovery/Tasks/Model capacity retry](../../ui/storybook/stories/recovery/model-capacity-retry.stories.tsx) (unreviewed), [Projects and execution/Recovery/Product/Issue Scheduled retry surfaces](../../ui/storybook/stories/recovery/scheduled-retry.stories.tsx) (unreviewed) - `recovery/source-action`: [Projects and execution/Recovery/Design previews/Recovery notice](../../ui/storybook/stories/recovery/recovery-notice-preview.stories.tsx) (current), [Projects and execution/Recovery/Paperclip/Source Issue Recovery](../../ui/storybook/stories/recovery/source-issue-recovery.stories.tsx) (unreviewed), [Projects and execution/Recovery/Paperclip/Source-resolved Fold](../../ui/storybook/stories/recovery/source-resolved-fold.stories.tsx) (unreviewed), [Projects and execution/Recovery/Workspace finalization recovery/01 Notice](../../ui/storybook/stories/recovery/workspace-finalization-recovery/01-notice.stories.tsx) (current), [Projects and execution/Recovery/Workspace finalization recovery/02 Task journey](../../ui/storybook/stories/recovery/workspace-finalization-recovery/02-task-journey.stories.tsx) (unreviewed) -- `recovery/exhaustion`: No cataloged story. +- `recovery/exhaustion`: [Projects and execution/Recovery/Exhaustion and user decisions](../../ui/storybook/stories/recovery/recovery-gap-states.stories.tsx) (current) - `recovery/workspace`: [Projects and execution/Recovery/Workspace branch recovery/01 Repair card](../../ui/storybook/stories/recovery/workspace-branch-recovery/01-component.stories.tsx) (unreviewed), [Projects and execution/Recovery/Workspace branch recovery/02 Task journey](../../ui/storybook/stories/recovery/workspace-branch-recovery/02-task-journey.stories.tsx) (unreviewed), [Projects and execution/Recovery/Workspace finalization recovery/01 Notice](../../ui/storybook/stories/recovery/workspace-finalization-recovery/01-notice.stories.tsx) (current), [Projects and execution/Recovery/Workspace finalization recovery/02 Task journey](../../ui/storybook/stories/recovery/workspace-finalization-recovery/02-task-journey.stories.tsx) (unreviewed) -- `recovery/unknown-outcome`: No cataloged story. -- `recovery/user-continuation`: No cataloged story. +- `recovery/unknown-outcome`: [Projects and execution/Recovery/Exhaustion and user decisions](../../ui/storybook/stories/recovery/recovery-gap-states.stories.tsx) (current) +- `recovery/user-continuation`: [Tasks and conversations/Steering and queued messages/Accepted response delivery](../../ui/storybook/stories/steering/durable-response-queue.stories.tsx) (current) - `recovery/reconnect`: [Projects and execution/Recovery/App/Connection recovery](../../ui/storybook/stories/recovery/cloud-access-recovery.stories.tsx) (unreviewed) - `connection-setup/catalog`: [Apps, channels, and extensions/Connection setup/Apps/Canonical icon registry](../../ui/storybook/stories/connection-setup/AppLogoRegistry.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Catalog source selection](../../ui/storybook/stories/connection-setup/apps-catalog-source-selection.stories.tsx) (proposed), [Apps, channels, and extensions/Connection setup/Apps/Connect discoverability (PAP-11091)](../../ui/storybook/stories/connection-setup/apps-connect-discoverability.stories.tsx) (unreviewed), [Agents and reusable capabilities/Runs, harnesses, and model accounts/Connectors/Pool host](../../ui/storybook/stories/runs-adapters/connection-pool-connector.stories.tsx) (unreviewed) - `connection-setup/identity-and-access`: [Apps, channels, and extensions/Connection setup/Apps/Connections/Arcade](../../ui/storybook/stories/connection-setup/ArcadeConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Composio](../../ui/storybook/stories/connection-setup/ComposioConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Provider choice](../../ui/storybook/stories/connection-setup/ConnectionProviderChoice.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Executor](../../ui/storybook/stories/connection-setup/ExecutorConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Zapier](../../ui/storybook/stories/connection-setup/ZapierConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connect name at create time (PAP-11283)](../../ui/storybook/stories/connection-setup/apps-connect-name.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Reviews/PAP-17835 Personal connection identity](../../ui/storybook/stories/connection-setup/personal-connection-identity.stories.tsx) (unreviewed) @@ -686,64 +723,64 @@ References are navigation links, not proof of parity. An absent link means no ca - `connection-setup/failure`: [Apps, channels, and extensions/Connection setup/Apps/Connections/Arcade](../../ui/storybook/stories/connection-setup/ArcadeConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Composio](../../ui/storybook/stories/connection-setup/ComposioConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Executor](../../ui/storybook/stories/connection-setup/ExecutorConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Zapier](../../ui/storybook/stories/connection-setup/ZapierConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Enterpret connector](../../ui/storybook/stories/connection-setup/enterpret-connect-flow.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/iMessage Photon](../../ui/storybook/stories/chat-channels/imessage-photon.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Notion MCP connect flow (PAP-16650)](../../ui/storybook/stories/connection-setup/notion-connect-flow.stories.tsx) (unreviewed) - `app-permissions/agent-access`: [Apps, channels, and extensions/App access and action permissions/Tools/Permitted vs Installed](../../ui/storybook/stories/app-permissions/permitted-vs-installed.stories.tsx) (unreviewed) - `app-permissions/action-policy`: [Apps, channels, and extensions/Connection setup/Apps/Connections/Arcade](../../ui/storybook/stories/connection-setup/ArcadeConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Composio](../../ui/storybook/stories/connection-setup/ComposioConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Executor](../../ui/storybook/stories/connection-setup/ExecutorConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Zapier](../../ui/storybook/stories/connection-setup/ZapierConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/App access and action permissions/Tools & Access/ActionCard](../../ui/storybook/stories/app-permissions/action-card.stories.tsx) (unreviewed), [Apps, channels, and extensions/App access and action permissions/Apps/Action permissions test](../../ui/storybook/stories/app-permissions/action-permissions-test.stories.tsx) (unreviewed), [Apps, channels, and extensions/App access and action permissions/Tools/Permitted vs Installed](../../ui/storybook/stories/app-permissions/permitted-vs-installed.stories.tsx) (unreviewed) -- `app-permissions/catalog-change`: No cataloged story. +- `app-permissions/catalog-change`: [Apps, channels, and extensions/App access and action permissions/Review newly discovered actions](../../ui/storybook/stories/app-permissions/catalog-review.stories.tsx) (current) - `app-permissions/test-action`: [Apps, channels, and extensions/Connection setup/Apps/Connections/Arcade](../../ui/storybook/stories/connection-setup/ArcadeConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Composio](../../ui/storybook/stories/connection-setup/ComposioConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Executor](../../ui/storybook/stories/connection-setup/ExecutorConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Connection setup/Apps/Connections/Zapier](../../ui/storybook/stories/connection-setup/ZapierConnection.stories.tsx) (unreviewed), [Apps, channels, and extensions/App access and action permissions/Tools & Access/ActionCard](../../ui/storybook/stories/app-permissions/action-card.stories.tsx) (unreviewed), [Apps, channels, and extensions/App access and action permissions/Apps/Action permissions test](../../ui/storybook/stories/app-permissions/action-permissions-test.stories.tsx) (unreviewed) - `app-permissions/account-maintenance`: [Apps, channels, and extensions/App access and action permissions/Apps/Managed accounts](../../ui/storybook/stories/app-permissions/apps-managed-accounts.stories.tsx) (current) - `chat-channels/providers`: [Apps, channels, and extensions/External chat and email/Connections/AgentMail setup](../../ui/storybook/stories/chat-channels/agentmail-onboarding.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/Chat connection actions](../../ui/storybook/stories/chat-channels/chat-connection-actions.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connectors/GitHub bot management](../../ui/storybook/stories/chat-channels/github-bot-management/management.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Historical/GitHub chat prototypes](../../ui/storybook/stories/chat-channels/github-chat.stories.tsx) (historical), [Apps, channels, and extensions/External chat and email/Connections/GitHub and Code Review Bot](../../ui/storybook/stories/chat-channels/github-connections.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/iMessage Photon](../../ui/storybook/stories/chat-channels/imessage-photon.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/Slack/Automatic setup](../../ui/storybook/stories/chat-channels/slack-automatic-setup.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/Slack/Managed setup](../../ui/storybook/stories/chat-channels/slack-managed-setup.stories.tsx) (unreviewed) - `chat-channels/identity`: [Apps, channels, and extensions/External chat and email/Connections/Chat management](../../ui/storybook/stories/chat-channels/chat-management/chat-management.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/iMessage Photon](../../ui/storybook/stories/chat-channels/imessage-photon.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/Slack/Avatar in Settings](../../ui/storybook/stories/chat-channels/slack-avatar-settings.stories.tsx) (current), [Apps, channels, and extensions/External chat and email/Connections/Slack/Communication in Settings](../../ui/storybook/stories/chat-channels/slack-communication-settings.stories.tsx) (current) - `chat-channels/conversation`: [Apps, channels, and extensions/External chat and email/Connections/AgentMail tasks](../../ui/storybook/stories/chat-channels/agentmail.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/Chat management](../../ui/storybook/stories/chat-channels/chat-management/chat-management.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connectors/GitHub bot management](../../ui/storybook/stories/chat-channels/github-bot-management/management.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Historical/GitHub chat prototypes](../../ui/storybook/stories/chat-channels/github-chat.stories.tsx) (historical), [Apps, channels, and extensions/External chat and email/Connections/iMessage Photon](../../ui/storybook/stories/chat-channels/imessage-photon.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/Slack/Task tools](../../ui/storybook/stories/chat-channels/slack-tools.stories.tsx) (unreviewed) -- `chat-channels/attachments`: No cataloged story. +- `chat-channels/attachments`: [Apps, channels, and extensions/External chat and email/Task-linked files](../../ui/storybook/stories/chat-channels/channel-attachments.stories.tsx) (current) - `chat-channels/lifecycle`: [Apps, channels, and extensions/External chat and email/Connections/AgentMail settings](../../ui/storybook/stories/chat-channels/agentmail-settings.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/Chat management](../../ui/storybook/stories/chat-channels/chat-management/chat-management.stories.tsx) (unreviewed), [Apps, channels, and extensions/External chat and email/Connections/iMessage Photon](../../ui/storybook/stories/chat-channels/imessage-photon.stories.tsx) (unreviewed) - `gateways-profiles/gateway`: [Apps, channels, and extensions/Tool gateways and access profiles/Tools/Gateways (PAP-11182)](../../ui/storybook/stories/gateways-profiles/gateways-tab.stories.tsx) (unreviewed) - `gateways-profiles/profiles`: [Apps, channels, and extensions/Tool gateways and access profiles/Tools/Access profiles](../../ui/storybook/stories/gateways-profiles/access-profiles.stories.tsx) (unreviewed) - `gateways-profiles/client-connect`: [Apps, channels, and extensions/Connection setup/Components/Agent setup prompt](../../ui/storybook/stories/connection-setup/agent-setup-prompt.stories.tsx) (unreviewed), [Agents and reusable capabilities/Runs, harnesses, and model accounts/Assistant connections/Dot Runner](../../ui/storybook/stories/runs-adapters/dot-runner-connection.stories.tsx) (unreviewed), [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Manage connections](../../ui/storybook/stories/gateways-profiles/public-mcp-connections.stories.tsx) (current), [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Consent](../../ui/storybook/stories/gateways-profiles/public-mcp-consent.stories.tsx) (current), [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Device authorization](../../ui/storybook/stories/gateways-profiles/public-mcp-device.stories.tsx) (current), [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Start here](../../ui/storybook/stories/gateways-profiles/public-mcp-journey.stories.tsx) (unreviewed), [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Experimental setting](../../ui/storybook/stories/gateways-profiles/public-mcp-setting.stories.tsx) (unreviewed) -- `gateways-profiles/tokens`: No cataloged story. +- `gateways-profiles/tokens`: [Apps, channels, and extensions/Tool gateways and access profiles/Named gateway tokens](../../ui/storybook/stories/gateways-profiles/gateway-tokens.stories.tsx) (current) - `gateways-profiles/activity`: [Oversight and operation/Dashboards and audit trails/Tools/Activity (audit feed)](../../ui/storybook/stories/activity/activity-feed.stories.tsx) (unreviewed), [Oversight and operation/Dashboards and audit trails/Apps/App detail · Activity (lifecycle)](../../ui/storybook/stories/activity/app-activity-lifecycle.stories.tsx) (unreviewed) - `secrets/company-secrets`: [Apps, channels, and extensions/Secrets and proposals/Product/Secrets](../../ui/storybook/stories/secrets/secrets.stories.tsx) (unreviewed) - `secrets/user-secrets`: [Apps, channels, and extensions/Secrets and proposals/Product/User secrets](../../ui/storybook/stories/secrets/user-secrets.stories.tsx) (unreviewed) -- `secrets/proposals`: No cataloged story. +- `secrets/proposals`: [Apps, channels, and extensions/Secrets and proposals/Proposal review](../../ui/storybook/stories/secrets/proposal-review.stories.tsx) (current) - `secrets/vault-import`: [Apps, channels, and extensions/Secrets and proposals/Product/Secrets](../../ui/storybook/stories/secrets/secrets.stories.tsx) (unreviewed) - `secrets/runtime-delivery`: [Apps, channels, and extensions/Secrets and proposals/Task credential access/01 Notice](../../ui/storybook/stories/secrets/credential-access/01-notice.stories.tsx) (unreviewed), [Apps, channels, and extensions/Secrets and proposals/Task credential access/02 Task page](../../ui/storybook/stories/secrets/credential-access/02-task-page.stories.tsx) (unreviewed), [Apps, channels, and extensions/Secrets and proposals/Product/Environment Variables Editor](../../ui/storybook/stories/secrets/environment-variables-editor.stories.tsx) (unreviewed), [Automation and structured work/Routines, schedules, and triggers/Product/Routines · Secrets tab](../../ui/storybook/stories/routines/routine-secrets.stories.tsx) (unreviewed) -- `plugins/installation`: No cataloged story. -- `plugins/configuration`: No cataloged story. -- `plugins/contributions`: No cataloged story. -- `plugins/lifecycle`: No cataloged story. +- `plugins/installation`: [Apps, channels, and extensions/Plugins/Installation and lifecycle](../../ui/storybook/stories/plugins/plugin-management.stories.tsx) (current) +- `plugins/configuration`: [Apps, channels, and extensions/Plugins/Configuration](../../ui/storybook/stories/plugins/plugin-configuration.stories.tsx) (current) +- `plugins/contributions`: [Apps, channels, and extensions/Plugins/Contributed routes](../../ui/storybook/stories/plugins/plugin-contributions.stories.tsx) (current) +- `plugins/lifecycle`: [Apps, channels, and extensions/Plugins/Installation and lifecycle](../../ui/storybook/stories/plugins/plugin-management.stories.tsx) (current) - `routines/definition`: [Automation and structured work/Routines, schedules, and triggers/Product/Routines · Detail (variation C)](../../ui/storybook/stories/routines/routine-detail-c.stories.tsx) (unreviewed), [Automation and structured work/Routines, schedules, and triggers/Product/Routines · Secrets tab](../../ui/storybook/stories/routines/routine-secrets.stories.tsx) (unreviewed), [Automation and structured work/Routines, schedules, and triggers/Product/Routines · List (grouped cards)](../../ui/storybook/stories/routines/routines-list-groups.stories.tsx) (unreviewed) - `routines/triggers`: [Apps, channels, and extensions/Connection setup/PR reviews/Fireflies and app webhooks](../../ui/storybook/stories/connection-setup/fireflies-pr.stories.tsx) (unreviewed), [Automation and structured work/Routines, schedules, and triggers/Product/Routines/Webhooks UX preview](../../ui/storybook/stories/routines/routine-webhooks-ux.stories.tsx) (unreviewed), [Automation and structured work/Routines, schedules, and triggers/Product/Routines/Webhooks](../../ui/storybook/stories/routines/routine-webhooks.stories.tsx) (unreviewed) -- `routines/run-now`: No cataloged story. +- `routines/run-now`: [Automation and structured work/Routines, schedules, and triggers/Run now](../../ui/storybook/stories/routines/run-now.stories.tsx) (current) - `routines/history`: [Automation and structured work/Routines, schedules, and triggers/Product/Routines/Webhooks](../../ui/storybook/stories/routines/routine-webhooks.stories.tsx) (unreviewed) - `routines/management`: [Automation and structured work/Routines, schedules, and triggers/Product/Routines · Detail (variation C)](../../ui/storybook/stories/routines/routine-detail-c.stories.tsx) (unreviewed), [Automation and structured work/Routines, schedules, and triggers/Product/Routines · List (grouped cards)](../../ui/storybook/stories/routines/routines-list-groups.stories.tsx) (unreviewed) -- `pipelines/stages`: No cataloged story. -- `pipelines/items`: No cataloged story. -- `pipelines/review`: No cataloged story. -- `pipelines/learnings`: No cataloged story. -- `cases/browse`: No cataloged story. -- `cases/structure`: No cataloged story. -- `cases/history`: No cataloged story. -- `cases/task-links`: No cataloged story. -- `status-cards/authoring`: No cataloged story. -- `status-cards/generation`: No cataloged story. -- `status-cards/inspection`: No cataloged story. -- `status-cards/archive`: No cataloged story. +- `pipelines/stages`: [Automation and structured work/Pipelines, review queues, and learnings/Stage settings](../../ui/storybook/stories/pipelines/stage-settings.stories.tsx) (current) +- `pipelines/items`: [Automation and structured work/Pipelines, review queues, and learnings/Items](../../ui/storybook/stories/pipelines/pipeline-board.stories.tsx) (current) +- `pipelines/review`: [Automation and structured work/Pipelines, review queues, and learnings/Review queue](../../ui/storybook/stories/pipelines/review-queue.stories.tsx) (current) +- `pipelines/learnings`: [Automation and structured work/Pipelines, review queues, and learnings/Learnings](../../ui/storybook/stories/pipelines/learnings.stories.tsx) (current) +- `cases/browse`: [Automation and structured work/Cases/Browse and hierarchy](../../ui/storybook/stories/cases/case-browse.stories.tsx) (current) +- `cases/structure`: [Automation and structured work/Cases/Structure, history and task links](../../ui/storybook/stories/cases/case-detail.stories.tsx) (current) +- `cases/history`: [Automation and structured work/Cases/Structure, history and task links](../../ui/storybook/stories/cases/case-detail.stories.tsx) (current) +- `cases/task-links`: [Automation and structured work/Cases/Structure, history and task links](../../ui/storybook/stories/cases/case-detail.stories.tsx) (current) +- `status-cards/authoring`: [Automation and structured work/Status cards/Authoring, updates and archive](../../ui/storybook/stories/status-cards/status-card-states.stories.tsx) (current) +- `status-cards/generation`: [Automation and structured work/Status cards/Authoring, updates and archive](../../ui/storybook/stories/status-cards/status-card-states.stories.tsx) (current) +- `status-cards/inspection`: [Automation and structured work/Status cards/Authoring, updates and archive](../../ui/storybook/stories/status-cards/status-card-states.stories.tsx) (current) +- `status-cards/archive`: [Automation and structured work/Status cards/Authoring, updates and archive](../../ui/storybook/stories/status-cards/status-card-states.stories.tsx) (current) - `budgets-costs/cost-reporting`: [Oversight and operation/Costs and budgets/Product/Budget & Finance](../../ui/storybook/stories/budgets-costs/budget-finance.stories.tsx) (unreviewed), [Oversight and operation/Costs and budgets/Costs/Subscriptions](../../ui/storybook/stories/budgets-costs/costs/subscriptions.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Decision models/01 Settings](../../ui/storybook/stories/companies/decision-models/01-settings.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Decision models/02 History](../../ui/storybook/stories/companies/decision-models/02-history.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Decision models/03 Journey](../../ui/storybook/stories/companies/decision-models/03-journey.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Fast responses/01 Settings](../../ui/storybook/stories/companies/fast-responses/01-settings.stories.tsx) (unreviewed), [Getting started and access/Companies and portability/Fast responses/02 History](../../ui/storybook/stories/companies/fast-responses/02-history.stories.tsx) (current) - `budgets-costs/budgets`: [Oversight and operation/Costs and budgets/Product/Budget & Finance](../../ui/storybook/stories/budgets-costs/budget-finance.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Product/Control Plane Surfaces](../../ui/storybook/stories/tasks/control-plane-surfaces.stories.tsx) (unreviewed), [Oversight and operation/Costs and budgets/Costs/Subscriptions](../../ui/storybook/stories/budgets-costs/costs/subscriptions.stories.tsx) (unreviewed) - `budgets-costs/incidents`: [Oversight and operation/Costs and budgets/Product/Budget & Finance](../../ui/storybook/stories/budgets-costs/budget-finance.stories.tsx) (unreviewed) -- `budgets-costs/resolution`: No cataloged story. +- `budgets-costs/resolution`: [Oversight and operation/Costs and budgets/Resolve a budget incident](../../ui/storybook/stories/budgets-costs/budget-resolution.stories.tsx) (current) - `activity/overview`: [Oversight and operation/Dashboards and audit trails/Product/Data Visualization & Misc](../../ui/storybook/stories/activity/data-viz-misc.stories.tsx) (unreviewed) - `activity/activity`: [Oversight and operation/Dashboards and audit trails/Tools/Activity (audit feed)](../../ui/storybook/stories/activity/activity-feed.stories.tsx) (unreviewed), [Oversight and operation/Dashboards and audit trails/Apps/App detail · Activity (lifecycle)](../../ui/storybook/stories/activity/app-activity-lifecycle.stories.tsx) (unreviewed), [Tasks and conversations/Task creation and lifecycle/Product/Control Plane Surfaces](../../ui/storybook/stories/tasks/control-plane-surfaces.stories.tsx) (unreviewed) -- `activity/runs`: No cataloged story. +- `activity/runs`: [Oversight and operation/Dashboards and audit trails/Execution history](../../ui/storybook/stories/activity/run-audit.stories.tsx) (current) - `activity/routine-audit`: [Automation and structured work/Routines, schedules, and triggers/Product/Routines · Detail (variation C)](../../ui/storybook/stories/routines/routine-detail-c.stories.tsx) (unreviewed), [Automation and structured work/Routines, schedules, and triggers/Product/Routines/Webhooks](../../ui/storybook/stories/routines/routine-webhooks.stories.tsx) (unreviewed) - `activity/timeline`: [Oversight and operation/Dashboards and audit trails/Pages/Work Timeline](../../ui/storybook/stories/activity/work-timeline.stories.tsx) (unreviewed) - `navigation-preferences/navigation`: [Oversight and operation/Navigation, profile, and announcements/Prototypes/Task detail/Mobile tabs](../../ui/storybook/stories/navigation-preferences/mobile-task-tabs.stories.tsx) (proposed), [Oversight and operation/Navigation, profile, and announcements/Product/Navigation & Layout](../../ui/storybook/stories/navigation-preferences/navigation-layout.stories.tsx) (unreviewed) -- `navigation-preferences/preferences`: No cataloged story. +- `navigation-preferences/preferences`: [Oversight and operation/Navigation, profile, and announcements/Sidebar preferences](../../ui/storybook/stories/navigation-preferences/sidebar-preferences.stories.tsx) (current) - `navigation-preferences/profile`: [Agents and reusable capabilities/Hiring, configuration, and organization/Agents/Personas/Full pages](../../ui/storybook/stories/agents/agent-persona-pages.stories.tsx) (unreviewed), [Agents and reusable capabilities/Hiring, configuration, and organization/Agents/Personas](../../ui/storybook/stories/agents/agent-personas.stories.tsx) (unreviewed) - `navigation-preferences/announcements`: [Oversight and operation/Navigation, profile, and announcements/Announcements/AnnouncementCard](../../ui/storybook/stories/navigation-preferences/announcement-card.stories.tsx) (unreviewed) - `navigation-preferences/fallbacks`: No cataloged story. - `instance-operations/install-update`: No cataloged story. - `instance-operations/server-service`: [Oversight and operation/Instance operations/Product/Dev Ops Surfaces](../../ui/storybook/stories/instance-operations/dev-ops-surfaces.stories.tsx) (unreviewed) -- `instance-operations/configuration`: [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Experimental setting](../../ui/storybook/stories/gateways-profiles/public-mcp-setting.stories.tsx) (unreviewed) +- `instance-operations/configuration`: [Apps, channels, and extensions/Tool gateways and access profiles/Assistant connections/Experimental setting](../../ui/storybook/stories/gateways-profiles/public-mcp-setting.stories.tsx) (unreviewed), [Oversight and operation/Instance operations/Backup retention](../../ui/storybook/stories/instance-operations/backup-retention.stories.tsx) (current) - `instance-operations/diagnostics`: No cataloged story. -- `instance-operations/backup`: No cataloged story. +- `instance-operations/backup`: [Oversight and operation/Instance operations/Backup retention](../../ui/storybook/stories/instance-operations/backup-retention.stories.tsx) (current) - `cli-operations/context`: No cataloged story. - `cli-operations/domain-commands`: No cataloged story. - `cli-operations/outputs`: No cataloged story. diff --git a/maps/storybook-map/REVIEW.md b/maps/storybook-map/REVIEW.md index 5ac927e7e91..95aa427f4d6 100644 --- a/maps/storybook-map/REVIEW.md +++ b/maps/storybook-map/REVIEW.md @@ -713,3 +713,119 @@ Those four recipe lines were inspected and the entries retain their fixture and historical limits; the stories do not establish that guard. Refreshed the five affected entry hashes and shared setup. The verification-route inventory still exactly matches all 60 unassociated capabilities, with no stale or missing rows. + +## Production-subject UI gap examples — 2026-10-13 + +Step 3 adds 37 suites and 257 scenarios associated with the 49 UI capabilities +from [VERIFICATION.md](VERIFICATION.md). Subjects are shipped pages/components; +API replies, sockets, callbacks and storage seeds remain simulated. Source +review uses production snapshot `a9ef9e8ebc1f41bb7047142704655d08b3e47be7`; +the story revision is `36689fc8149c31858d0b4c6f7839ecc663404073` plus the review +corrections in this commit. Catalog fingerprints identify the tested source bytes. +Existing suite classifications and all 2,248 prior built entry IDs are preserved; +the new built index has 2,505 entries. Existing unreviewed suites remain unreviewed. + +Executed the focused browser suite against this checkout's static build on macOS +Chromium at 1280 × 900 with reduced motion: **260 checks passed**. Every new story +was exercised: 129 declared play functions passed; the remaining 128 scenarios +have explicit subject-content, skeleton, editable-control or documented fallback +assertions. The only intentionally empty root is the zero-entry durable-response +queue, whose shipped component returns null. Additional checks verified token +fixture reset through sidebar navigation and backup retention at 390 × 844 in +light and dark themes. No browser page errors were observed. The test names +identify each story ID; rerun with the command in VERIFICATION.md. Selected +screenshots are inspection captures, not pinned Linux visual-baseline comparisons. + +Review corrections give empty stage settings zero stages and assert its message, +make failed reads return HTTP 500 rather than the service-warmup HTTP 503, +provide Inbox dependencies and actual plugin redirect destinations, use distinct +attachment paths and a valid uncertain-submission UUID, and select Interactions +before play starts so the browser harness does not steal input/menu focus. + +Repository typecheck/build, the UI Vitest suite (730 files, 8,208 tests), +four fixture-boundary tests, 27 catalog/generator/map tests, token gates and +Storybook build passed. The full local repository test command could not complete: +embedded PostgreSQL exhausted host shared-memory allocation, and native-runner +tests reported timeouts. No host settings or other jobs were changed. + +Production acceptance and pinned Linux visual comparisons were not run. The +scenario inventory retains outstanding recipe variants and real authorization, +persistence and provider contracts. Examples expose existing read-error/loading +fallbacks rather than inventing feedback; those limits are named in VERIFICATION.md. + +## Step-3 merge reconciliation — 2026-10-13 + +Merged current master `da9af67415` through the verification parent. Preserved +upstream story additions, review notes and classifications and all 37 new suites. +Compared changed catalog inputs with the reviewed step-2 snapshot: five feature +recipes, the generated feature index, BudgetIncidentCard/Costs, Inbox and the +recovery notice wiring changed. The two preview-hook lines remain as reviewed. +The new budget fixtures explicitly use hard thresholds; their action labels and +callbacks are retained. Inbox fixtures use human-created tasks; unrelated +automation discovery remains outside their asserted scope. Recovery fixtures use +generic exhaustion/unknown-outcome causes, not the newly gated native-owner path. +Recipe additions concern Telegram maintenance, liveness versus readiness, trusted +instance-secret lifecycle and native copyback verification, which these isolated +UI stories do not establish. Reviewed those differences, refreshed affected +fingerprints with scoped notes, and retained all statuses. No new live acceptance +or rendering evidence is claimed for the merge; the earlier 260-check pass names +its tested revision. Catalog/map validation and 27 targeted tests pass. + +## Draft-fixture isolation correction before merge — 2026-10-13 + +After #16167 merged as `252d9b5077`, retained the step-3 catalog and scenarios +while reconciling its squash-merged verification file. Review found that +`clearDraft` correctly refuses to clear a retained submission without its +matching attempt ID. Story setup/cleanup now passes that ID for the two +Storybook-owned draft keys before reseeding them. Production draft guards are +unchanged. The new same-browser check switches from UncertainSubmission to +Empty, SentClearsDraft and SendFailedKeepsDraft; it asserts the warning disappears +and the expected empty or retained draft is shown, with declared play passing. +All six draft stories plus that switching regression passed (seven browser +checks) on the owned macOS Chromium development preview after cold dependency +optimization. UI typecheck, 16 existing draft-persistence tests and the 27 +catalog/generator/map tests passed. No whole-suite browser rerun, production +submission journey or visual-baseline comparison is claimed for this narrow fix. + +Static merge verification: Storybook build passed after restoring the checkout's +renderer dependencies. All 2,248 original built IDs remain in the 2,517-entry +index. The 25 budget/inbox/recovery examples affected by upstream source changes, +six draft examples and same-browser reset check passed on the static preview +(32 checks). The reset check uses the sidebar for Empty and reloads send stories +within the same browser/storage context, so it cannot read the prior story's +completed play status while the new play starts. No isolated-context substitute +is used for the retained-submission regression. The unchanged GitHub bot-management +suite also passed locally (34 tests) after its CI assertion failed; a separate +server job reported cancellation. Those CI jobs require a fresh passing run. + +## Named play-result assertions — 2026-10-13 + +Strengthened four review cases: UnavailableCompanyDraft waits for the visible +wizard and an empty agent-name input before rejecting the foreign draft; Probing +asserts a disabled Testing button; FilterNoMatches asserts the filtered empty +message and absence of the seeded run; Delivering asserts the queue's optimistic +removal after clicking a loaded row. The shipped queue returns null while that +delivery remains pending, so this story records the missing visible progress +presentation rather than inventing it. All four browser cases passed on the +owned preview, as did UI typecheck and 27 catalog/generator/map tests. + +## Codex and UI-gap catalog reconciliation — 2026-10-13 + +Reconciled Codex `6c0eca871616a110954553d6ff971e7655c85f93` with master +`c8efdbfba43377d39f57e4822e92d796a8a044b6`. Retained all 226 existing suites +and all 37 incoming UI-gap suites, both dated review histories, stable IDs, +classifications and simulation boundaries. The catalog now contains 263 authored +files and 2,499 stories. The incoming verification routes remain separate from +production acceptance. Earlier browser results retain their exact tested sources. + +Inspected the combined inputs before refreshing three new-suite fingerprints +and shared setup. The backup recipe paragraph only moves to operator verification; +the merged import page and onboarding wizard retain exact Codex source bytes. +Their new fixture notes name the native setup, import persistence, credential, +provider and cleanup journeys these isolated stories do not prove. No application, +package or native source was changed by this reconciliation. + +The existing catalog generator/checker, all 27 catalog/generator/map controls and +the 35-area/175-capability map check pass. This is source inventory and structural +verification only: no additional rendering, play, visual baseline, paid provider +call or live production acceptance was performed. diff --git a/maps/storybook-map/VERIFICATION.md b/maps/storybook-map/VERIFICATION.md index 0ce0ef8646b..8a18ec2479c 100644 --- a/maps/storybook-map/VERIFICATION.md +++ b/maps/storybook-map/VERIFICATION.md @@ -1,18 +1,79 @@ -# Verification routes for Storybook gaps +# Verification routes for Storybook capabilities -Reviewed 2026-10-13 against source commit `5e8e992df2b3d71f82091b7bbf29fd482854d9f5` (merged #16154, including #16153 task notifications). +Updated 2026-10-13. Production source is reconciled with master `252d9b5077`; step-3 story and fixture changes are recorded in the catalog fingerprints. -The catalog has 223 authored suites, 2,212 authored story IDs and 175 feature-map capabilities. Of those capabilities, 60 have no associated example suite: **49 need authored UI examples**, **8 need CLI/operator verification**, and **3 primarily need real browser verification**. Generated Overview pages and guides do not count as examples. +The catalog has 262 authored suites, 2,481 authored story IDs and 175 feature-map capabilities. The 37 new production-subject suites add 257 scenarios and associate all 49 UI capabilities identified in step 2. **11 capabilities remain without authored examples: 8 CLI/operator operations and 3 real-browser concerns.** Generated Overview pages and guides do not count as examples. Existing suites and story IDs are retained. -This is step 2 of the Storybook reconciliation: choose and record the verification surface. It is a source classification of the 60 unassociated capabilities, not a full parity review of all suites or a live verification pass. All execution in this inventory is **not run**; test links identify existing starting points, not new passing evidence or exhaustive coverage. The 115 capabilities with example links still need production tests for their non-visual contracts. +These associations provide initial isolated UI coverage, not complete parity with every feature recipe. The original scenario inventory below stays as a maintenance checklist; an association does not mark every listed variant complete. In particular, import examples cover preview, organization archival examples cover the list/restoration side, and custom-image examples cover metadata/configuration rather than a real setup terminal. Existing unreviewed suites remain unreviewed. -A story can establish component rendering, mock contract and asserted interaction. It cannot establish real authentication, permissions, persisted changes, run continuation, provider delivery or installation. Never classify a mixed capability as CLI/API-only merely because its backend needs those checks. +A story can establish component rendering, fixture shape and asserted interaction. It cannot establish real authentication, permissions, persisted changes, run continuation, provider delivery or installation. **The production journeys listed below have not been run as part of this Storybook pass.** Existing test links identify starting points, not exhaustive or passing production acceptance evidence. -## UI examples to add in step 3 +## New example suites -These 49 capabilities need shipped-component examples with realistic mocked boundaries. Cover loading, empty, failure, permission and successful states where meaningful; do not invent unsupported controls. Display-first rows still retain the listed production checks. Story creation and full existing-suite review are subsequent work. +All subjects use shipped components. APIs, socket messages, callbacks and storage seeds are simulated. Review results and source limitations are recorded in the catalog; the focused browser command is: -| Capability | Missing isolated UI evidence | Separate production evidence | Source and existing test starting points | +```sh +pnpm build-storybook +node scripts/serve-storybook-static.mjs --port 6136 +# In a second terminal: +pnpm exec playwright test --config tests/storybook-visual/feature-gap-stories.config.ts +``` + +The static server must be an owned process serving this checkout. Override the browser target with `PAPERCLIP_GAP_STORYBOOK_URL` when using another owned port. The browser suite asserts each non-interactive story's expected content, skeleton or documented fallback and waits for declared play assertions; it also checks reset when switching token and uncertain-draft stories in one browser. This is not the pinned Linux visual-baseline comparison. + +| Suite | Capability associations | Scenarios / play functions | +| --- | --- | --- | +| [Getting started and access/Login, invitations, and access/CLI authorization](../../ui/storybook/stories/access/cli-authorization.stories.tsx) | `access/cli-auth` | 9 / 5 | +| [Getting started and access/Login, invitations, and access/Organization membership](../../ui/storybook/stories/access/membership.stories.tsx) | `access/membership` | 7 / 2 | +| [Getting started and access/Login, invitations, and access/Sign in](../../ui/storybook/stories/access/sign-in.stories.tsx) | `access/login` | 6 / 3 | +| [Oversight and operation/Dashboards and audit trails/Execution history](../../ui/storybook/stories/activity/run-audit.stories.tsx) | `activity/runs` | 6 / 4 | +| [Tasks and conversations/Agent conversations and project handoff/Conference Room](../../ui/storybook/stories/agent-chat/conference-room.stories.tsx) | `agent-chat/conference-room` | 5 / 4 | +| [Apps, channels, and extensions/App access and action permissions/Review newly discovered actions](../../ui/storybook/stories/app-permissions/catalog-review.stories.tsx) | `app-permissions/catalog-change` | 7 / 2 | +| [Oversight and operation/Costs and budgets/Resolve a budget incident](../../ui/storybook/stories/budgets-costs/budget-resolution.stories.tsx) | `budgets-costs/resolution` | 10 / 6 | +| [Automation and structured work/Cases/Browse and hierarchy](../../ui/storybook/stories/cases/case-browse.stories.tsx) | `cases/browse` | 5 / 1 | +| [Automation and structured work/Cases/Structure, history and task links](../../ui/storybook/stories/cases/case-detail.stories.tsx) | `cases/structure`, `cases/history`, `cases/task-links` | 8 / 4 | +| [Apps, channels, and extensions/External chat and email/Task-linked files](../../ui/storybook/stories/chat-channels/channel-attachments.stories.tsx) | `chat-channels/attachments` | 5 / 2 | +| [Getting started and access/Companies and portability/Import preview](../../ui/storybook/stories/companies/company-import.stories.tsx) | `companies/portability` | 6 / 5 | +| [Getting started and access/Companies and portability/Selection and archived organizations](../../ui/storybook/stories/companies/company-selection.stories.tsx) | `companies/selection`, `companies/archive` | 7 / 2 | +| [Tasks and conversations/Documents, attachments, and work products/Document revisions](../../ui/storybook/stories/documents-artifacts/document-revisions.stories.tsx) | `documents-artifacts/documents` | 9 / 4 | +| [Projects and execution/Execution environments/Custom images](../../ui/storybook/stories/execution-environments/custom-images.stories.tsx) | `execution-environments/images` | 6 / 3 | +| [Projects and execution/Execution environments/Configuration, capabilities, and probes](../../ui/storybook/stories/execution-environments/environment-states.stories.tsx) | `execution-environments/configuration`, `execution-environments/capabilities`, `execution-environments/probe` | 10 / 4 | +| [Apps, channels, and extensions/Tool gateways and access profiles/Named gateway tokens](../../ui/storybook/stories/gateways-profiles/gateway-tokens.stories.tsx) | `gateways-profiles/tokens` | 8 / 5 | +| [Projects and execution/Goals and work alignment/Aligned work and navigation](../../ui/storybook/stories/goals/goal-alignment.stories.tsx) | `goals/alignment`, `goals/navigation` | 6 / 2 | +| [Tasks and conversations/Inbox, decisions, and search/Personal task views](../../ui/storybook/stories/inbox-search/inbox-views.stories.tsx) | `inbox-search/views` | 8 / 0 | +| [Tasks and conversations/Inbox, decisions, and search/Personal task notifications](../../ui/storybook/stories/inbox-search/personal-task-toasts.stories.tsx) | `inbox-search/toasts` | 4 / 4 | +| [Oversight and operation/Instance operations/Backup retention](../../ui/storybook/stories/instance-operations/backup-retention.stories.tsx) | `instance-operations/backup`, `instance-operations/configuration` | 6 / 6 | +| [Oversight and operation/Navigation, profile, and announcements/Sidebar preferences](../../ui/storybook/stories/navigation-preferences/sidebar-preferences.stories.tsx) | `navigation-preferences/preferences` | 5 / 1 | +| [Getting started and access/Onboarding and first work/Resume a saved draft](../../ui/storybook/stories/onboarding/resume-draft.stories.tsx) | `onboarding/resume` | 4 / 3 | +| [Automation and structured work/Pipelines, review queues, and learnings/Learnings](../../ui/storybook/stories/pipelines/learnings.stories.tsx) | `pipelines/learnings` | 5 / 1 | +| [Automation and structured work/Pipelines, review queues, and learnings/Items](../../ui/storybook/stories/pipelines/pipeline-board.stories.tsx) | `pipelines/items` | 5 / 1 | +| [Automation and structured work/Pipelines, review queues, and learnings/Review queue](../../ui/storybook/stories/pipelines/review-queue.stories.tsx) | `pipelines/review` | 7 / 3 | +| [Automation and structured work/Pipelines, review queues, and learnings/Stage settings](../../ui/storybook/stories/pipelines/stage-settings.stories.tsx) | `pipelines/stages` | 5 / 2 | +| [Apps, channels, and extensions/Plugins/Configuration](../../ui/storybook/stories/plugins/plugin-configuration.stories.tsx) | `plugins/configuration` | 6 / 3 | +| [Apps, channels, and extensions/Plugins/Contributed routes](../../ui/storybook/stories/plugins/plugin-contributions.stories.tsx) | `plugins/contributions` | 4 / 1 | +| [Apps, channels, and extensions/Plugins/Installation and lifecycle](../../ui/storybook/stories/plugins/plugin-management.stories.tsx) | `plugins/installation`, `plugins/lifecycle` | 10 / 5 | +| [Projects and execution/Recovery/Exhaustion and user decisions](../../ui/storybook/stories/recovery/recovery-gap-states.stories.tsx) | `recovery/exhaustion`, `recovery/unknown-outcome` | 7 / 7 | +| [Automation and structured work/Routines, schedules, and triggers/Run now](../../ui/storybook/stories/routines/run-now.stories.tsx) | `routines/run-now` | 8 / 4 | +| [Apps, channels, and extensions/Secrets and proposals/Proposal review](../../ui/storybook/stories/secrets/proposal-review.stories.tsx) | `secrets/proposals` | 7 / 3 | +| [Agents and reusable capabilities/Skills and Skill Studio/Test runs](../../ui/storybook/stories/skills/skill-test-runs.stories.tsx) | `skills/testing` | 9 / 1 | +| [Automation and structured work/Status cards/Authoring, updates and archive](../../ui/storybook/stories/status-cards/status-card-states.stories.tsx) | `status-cards/authoring`, `status-cards/generation`, `status-cards/inspection`, `status-cards/archive` | 17 / 10 | +| [Tasks and conversations/Steering and queued messages/Draft recovery](../../ui/storybook/stories/steering/composer-draft-recovery.stories.tsx) | `steering/drafts` | 6 / 3 | +| [Tasks and conversations/Steering and queued messages/Accepted response delivery](../../ui/storybook/stories/steering/durable-response-queue.stories.tsx) | `steering/approval-queue`, `recovery/user-continuation` | 7 / 6 | +| [Projects and execution/Workspaces, services, and files/Close readiness](../../ui/storybook/stories/workspaces/workspace-close.stories.tsx) | `workspaces/closure` | 7 / 7 | + +## Production UI limitations exposed + +- Plugin contribution discovery errors remain on “Loading…”. The named DiscoveryFailure story exposes this shipped behavior; it does not claim a useful failure state. +- The document section and execution-environment list lack dedicated read-error/loading presentation. Cases and Inbox also fall back to empty views after list errors. Their stories retain the real fallback behavior. Custom-image metadata has its own loading and error states. +- GoalDetail displays aligned projects and children. The current UI has no project goal-assignment control. The stories do not invent one. +- Accepted queued responses disappear optimistically while delivery is pending; the shipped component has no separate visible progress indicator. The Delivering story asserts this presentation. +- Conference Room maps a denied stream to its generic assistant-unavailable message. Real permission enforcement belongs to the separate route verification. + +## UI scenarios and separate production contracts + +These 49 capabilities now have initial shipped-component examples. Keep the requested scenarios below as the target for continued maintenance. Loading, empty, failure, permission and successful states are appropriate only when the production subject supports them. The separate production contracts remain required. + +| Capability | UI scenarios to maintain | Separate production evidence | Source and existing test starting points | | --- | --- | --- | --- | | [`onboarding/resume`](../feature-map/onboarding.md) | Restored incomplete wizard; invalid or unauthorized saved company; temporarily withheld draft; account switch; storage failure. | Reload company-prefixed onboarding without duplicating company; restore only authorized account/company draft and retain input after recoverable loading failure. | Source: [OnboardingWizard.tsx](../../ui/src/components/OnboardingWizard.tsx), [onboarding-state.ts](../../ui/src/lib/onboarding-state.ts). Tests: [OnboardingWizard.test.tsx](../../ui/src/components/OnboardingWizard.test.tsx), [onboarding-state.test.ts](../../ui/src/lib/onboarding-state.test.ts), [onboarding.spec.ts](../../tests/e2e/onboarding.spec.ts) | | [`access/login`](../feature-map/access.md) | Sign-in form, pending and invalid credentials, deployment-mode claim gate, expired claim. | Authenticate and reload company deep link; sign out and confirm session enforcement; reject invalid bootstrap claim. | Source: [Auth.tsx](../../ui/src/pages/Auth.tsx), [BoardClaim.tsx](../../ui/src/pages/BoardClaim.tsx), [board-claim.ts](../../server/src/board-claim.ts). Tests: [Auth.test.tsx](../../ui/src/pages/Auth.test.tsx), [auth-session-route.test.ts](../../server/src/__tests__/auth-session-route.test.ts), [board-claim.test.ts](../../server/src/__tests__/board-claim.test.ts) | diff --git a/maps/storybook-map/catalog.json b/maps/storybook-map/catalog.json index ebf259c221c..6106206d2b5 100644 --- a/maps/storybook-map/catalog.json +++ b/maps/storybook-map/catalog.json @@ -8,9 +8,10 @@ "maps/feature-map/README.md", "scripts/storybook-feature-index.mjs", "ui/storybook/generated/feature-index.json", - "ui/storybook/support/feature-map/FeatureOverview.tsx" + "ui/storybook/support/feature-map/FeatureOverview.tsx", + "ui/storybook/support/capability-gaps/api.ts" ], - "fingerprint": "9eb1ee102be596dfe61c472e3dce70839061401b1a62036a558c0239f57ba2a1" + "fingerprint": "67b8c3a8e708c2e3bcd8a4a77d72e1f6575b5084212c288cfd1abab2118fce50" }, "entries": [ { @@ -5790,6 +5791,907 @@ "fingerprint": "5055ac064b22cda06d70b9f41049f0c8fb22cb94f35398cb0690bc13980b22d9", "stableId": "agents-codex-runner-creation", "sidebarTitle": "Agents and reusable capabilities/Hiring, configuration, and organization/Agents/Codex runner/Creation" + }, + { + "file": "ui/storybook/stories/instance-operations/backup-retention.stories.tsx", + "capabilities": [ + "instance-operations/backup", + "instance-operations/configuration" + ], + "purpose": "Oversight and operation/Instance operations/Backup retention", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Retention presets, initial read, loading, error, denial and pending/saved selection in InstanceGeneralSettings. Backup creation/restore and real settings authority remain production obligations. The recipe distinguishes process liveness from authenticated database readiness; these examples do not exercise that operator API boundary. The Codex branch relocates the liveness/readiness recipe paragraph to operator verification without changing its contract; this suite retains separate backup/restore and live readiness obligations.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/instance-operations.md", + "ui/src/pages/InstanceGeneralSettings.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/instance-operations/backup-retention.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-backup-retention", + "sidebarTitle": "Oversight and operation/Instance operations/Backup retention", + "fingerprint": "84073c2062227715cae2cc3d712c0bae8cbb15eedcf255ad652068a9efca5608" + }, + { + "file": "ui/storybook/stories/recovery/recovery-gap-states.stories.tsx", + "capabilities": [ + "recovery/exhaustion", + "recovery/unknown-outcome" + ], + "purpose": "Projects and execution/Recovery/Exhaustion and user decisions", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Exhaustion notice, blocked authority, retry pending/failure/accepted and inactive notice; unknown-outcome action card is rendered separately. Retry callbacks and disposition context are simulated; no real continuation proof. The expanded native copyback recovery recipe requires harness stop proof; these generic exhaustion/queued-response fixtures do not establish that path.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/recovery.md", + "ui/src/components/DispositionRecoveryNotice.tsx", + "ui/src/components/IssueRecoveryActionCard.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/fixtures/paperclipData.ts", + "ui/storybook/stories/recovery/recovery-gap-states.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-recovery-gap-states", + "sidebarTitle": "Projects and execution/Recovery/Exhaustion and user decisions", + "fingerprint": "b040d5167216510cd7f4384294fb27eb0a06168436b9cdd0a1ea9ac1b0e997b0" + }, + { + "file": "ui/storybook/stories/steering/durable-response-queue.stories.tsx", + "capabilities": [ + "steering/approval-queue", + "recovery/user-continuation" + ], + "purpose": "Tasks and conversations/Steering and queued messages/Accepted response delivery", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Accepted immutable response, cleanup wait, unavailable admission, empty queue and interrupt delivery states. Local callbacks remove delivered entries; durable queue/coalescing and fresh-session authority need production tests. The expanded native copyback recovery recipe requires harness stop proof; these generic exhaustion/queued-response fixtures do not establish that path. The shipped pending-delivery path removes rows optimistically and shows no separate progress indicator; the Delivering story asserts that actual presentation.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/recovery.md", + "maps/feature-map/steering.md", + "ui/src/components/task-chat/TaskChatQueuedMessages.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/steering/durable-response-queue.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-durable-response-queue", + "sidebarTitle": "Tasks and conversations/Steering and queued messages/Accepted response delivery", + "fingerprint": "0071986d1378afd66fcb683d479d9569b05af97a4f98b2bc7ced1f67e8aaf61b" + }, + { + "file": "ui/storybook/stories/workspaces/workspace-close.stories.tsx", + "capabilities": [ + "workspaces/closure" + ], + "purpose": "Projects and execution/Workspaces, services, and files/Close readiness", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Readiness dialog with loading, inspection failure/403, running-task block and pending/accepted archive response. No real workspace cleanup, Git removal or service shutdown is performed.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/workspaces.md", + "ui/src/components/ExecutionWorkspaceCloseDialog.tsx", + "ui/src/components/ToastViewport.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/workspaces/workspace-close.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-workspace-close", + "sidebarTitle": "Projects and execution/Workspaces, services, and files/Close readiness", + "fingerprint": "b3d1d32d7f1f51f837ecd04f64ca1b6b7e8b36c921d54b638898ef06512617cc" + }, + { + "file": "ui/storybook/stories/budgets-costs/budget-resolution.stories.tsx", + "capabilities": [ + "budgets-costs/resolution" + ], + "purpose": "Oversight and operation/Costs and budgets/Resolve a budget incident", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Shipped Costs budget tab with empty/read failures, invalid amount, raise-and-resume, keep-paused and write failure/pending. Fixture incident transitions only; real overlapping holds and hard stops remain separate. The upstream budget UI distinguishes soft warnings from hard stops; hard incidents retain raise-and-resume and keep-paused controls.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/budgets-costs.md", + "ui/src/pages/Costs.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/budgets-costs/budget-resolution.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-budget-resolution", + "sidebarTitle": "Oversight and operation/Costs and budgets/Resolve a budget incident", + "fingerprint": "8cf347aa01c0b4cb1364735794581b9123a23aba2151278a1a83354d582053ef" + }, + { + "file": "ui/storybook/stories/activity/run-audit.stories.tsx", + "capabilities": [ + "activity/runs" + ], + "purpose": "Oversight and operation/Dashboards and audit trails/Execution history", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "AuditRuns list, completed receipt, empty/loading/failed/denied and no-match filter. Run records are fixture snapshots; no real execution or scoped audit proof.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/activity.md", + "ui/src/pages/audit/AuditRuns.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/fixtures/paperclipData.ts", + "ui/storybook/stories/activity/run-audit.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-run-audit", + "sidebarTitle": "Oversight and operation/Dashboards and audit trails/Execution history", + "fingerprint": "0a43211053a4c36b6ae4061a624b7e223660931b565a234a829b38d53eab4861" + }, + { + "file": "ui/storybook/stories/secrets/proposal-review.stories.tsx", + "capabilities": [ + "secrets/proposals" + ], + "purpose": "Apps, channels, and extensions/Secrets and proposals/Proposal review", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Pending proposal, authority preflight block, list states and rejected proposal/rejection error. ProposalsTab and its dialogs are shipped; secret creation, values and grant authority are not exercised. Agent-owned instance secrets remain in trusted server runtime and are excluded from ordinary agent delivery; these UI examples do not verify that lifecycle contract.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/secrets.md", + "ui/src/components/ToastViewport.tsx", + "ui/src/pages/secrets/ProposalsTab.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/secrets/proposal-review.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-secret-proposal-review", + "sidebarTitle": "Apps, channels, and extensions/Secrets and proposals/Proposal review", + "fingerprint": "eb3a67d3543f732a6be88f3a2ee9b070f171ff18a037f1869f64b3edd66dfb14" + }, + { + "file": "ui/storybook/stories/gateways-profiles/gateway-tokens.stories.tsx", + "capabilities": [ + "gateways-profiles/tokens" + ], + "purpose": "Apps, channels, and extensions/Tool gateways and access profiles/Named gateway tokens", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Named, empty and expired token history; issuance pending/error/403/success and name-confirmed revocation. Tokens are fictional; real bearer expiry, scope and revocation need API verification.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/gateways-profiles.md", + "ui/src/api/tools.ts", + "ui/src/components/ToastViewport.tsx", + "ui/src/pages/apps/gateways/NewGatewayDialog.tsx", + "ui/src/pages/apps/gateways/panels/TokensPanel.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/gateways-profiles/gateway-tokens.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-gateway-tokens", + "sidebarTitle": "Apps, channels, and extensions/Tool gateways and access profiles/Named gateway tokens", + "fingerprint": "0c565737015544b791cf7b4fda2f80fd9456368aeb8f0df0f9a4f60ad221b028" + }, + { + "file": "ui/storybook/stories/access/cli-authorization.stories.tsx", + "capabilities": [ + "access/cli-auth" + ], + "purpose": "Getting started and access/Login, invitations, and access/CLI authorization", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Real CliAuth route with pending/expired/unavailable/preflight denial, approval/cancel and write pending/error. Fictional challenge; no CLI identity or real token is authorized.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/access.md", + "ui/src/lib/router.tsx", + "ui/src/pages/CliAuth.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/access/cli-authorization.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-cli-authorization", + "sidebarTitle": "Getting started and access/Login, invitations, and access/CLI authorization", + "fingerprint": "c87b678d05b7818fd201f97c7e41cd92ce25520123ef4d20b56dd0c9785455ee" + }, + { + "file": "ui/storybook/stories/access/sign-in.stories.tsx", + "capabilities": [ + "access/login" + ], + "purpose": "Getting started and access/Login, invitations, and access/Sign in", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "AuthPage email form, session read loading/error, submission pending/failure and navigation to Companies after a fixture session. Real authentication, bootstrap claim and sign-out remain separate.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/access.md", + "ui/src/lib/router.tsx", + "ui/src/pages/Auth.tsx", + "ui/src/pages/Companies.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/fixtures/paperclipData.ts", + "ui/storybook/stories/access/sign-in.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-sign-in", + "sidebarTitle": "Getting started and access/Login, invitations, and access/Sign in", + "fingerprint": "b9524d5f5a97e14c7e3bc5989b94ebe0fea56ab8767df5c5efb24430b003d491" + }, + { + "file": "ui/storybook/stories/access/membership.stories.tsx", + "capabilities": [ + "access/membership" + ], + "purpose": "Getting started and access/Login, invitations, and access/Organization membership", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "CompanyAccess member list and role change with loading/empty/failure/403 and failed mutation toast. Backend actor authority, invite lifecycle and durable membership are not verified.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/access.md", + "ui/src/components/ToastViewport.tsx", + "ui/src/pages/CompanyAccess.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/access/membership.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-membership", + "sidebarTitle": "Getting started and access/Login, invitations, and access/Organization membership", + "fingerprint": "8bd65a1aa65dc4e4767390eefb1b731a410ff852ac6eced05f5e3e5249d1a356" + }, + { + "file": "ui/storybook/stories/companies/company-selection.stories.tsx", + "capabilities": [ + "companies/selection", + "companies/archive" + ], + "purpose": "Getting started and access/Companies and portability/Selection and archived organizations", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Companies list, selection storage and archived-company restoration with loading/empty/error/403. This covers the archive list/restoration side; CompanySettings archive confirmation remains an additional UI scenario.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/companies.md", + "ui/src/pages/Companies.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/fixtures/paperclipData.ts", + "ui/storybook/stories/companies/company-selection.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-company-selection", + "sidebarTitle": "Getting started and access/Companies and portability/Selection and archived organizations", + "fingerprint": "0275b0bd6538672ef381ffb6fbdf68b7611e93df97334246cb4b3b1c6856bc17" + }, + { + "file": "ui/storybook/stories/plugins/plugin-management.stories.tsx", + "capabilities": [ + "plugins/installation", + "plugins/lifecycle" + ], + "purpose": "Apps, channels, and extensions/Plugins/Installation and lifecycle", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "PluginManager list states, install pending/error/success, disable and uninstall receipts. Local record transitions only; package loading and worker lifecycle require production verification.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/plugins.md", + "ui/src/components/ToastViewport.tsx", + "ui/src/pages/PluginManager.tsx", + "ui/src/pages/PluginSettings.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/plugins/plugin-management.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts", + "ui/storybook/support/capability-gaps/plugins.ts" + ], + "stableId": "feature-plugin-management", + "sidebarTitle": "Apps, channels, and extensions/Plugins/Installation and lifecycle", + "fingerprint": "fd52b6f6966ae05ad7ffb755eebcfa0aeb6d9cb7c246a9def116de663eb5feeb" + }, + { + "file": "ui/storybook/stories/plugins/plugin-configuration.stories.tsx", + "capabilities": [ + "plugins/configuration" + ], + "purpose": "Apps, channels, and extensions/Plugins/Configuration", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "PluginSettings configuration route with hydrated values, read pending/unavailable and saved/error/403 responses. Real config storage and plugin worker validation are not proved.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/plugins.md", + "ui/src/lib/router.tsx", + "ui/src/pages/PluginManager.tsx", + "ui/src/pages/PluginSettings.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/plugins/plugin-configuration.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts", + "ui/storybook/support/capability-gaps/plugins.ts" + ], + "stableId": "feature-plugin-configuration", + "sidebarTitle": "Apps, channels, and extensions/Plugins/Configuration", + "fingerprint": "b80dc4cfa9311c9df6fb795a4b481405144342a9003062b2e0d8397d6f8b68e1" + }, + { + "file": "ui/storybook/stories/plugins/plugin-contributions.stories.tsx", + "capabilities": [ + "plugins/contributions" + ], + "purpose": "Apps, channels, and extensions/Plugins/Contributed routes", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Production PluginPage host with a registered fixture plugin component; pending/missing contribution and discovery failure. Discovery failure currently remains Loading; contribution contents are a fixture, not shipped plugin code.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/plugins.md", + "ui/src/lib/router.tsx", + "ui/src/pages/PluginManager.tsx", + "ui/src/pages/PluginPage.tsx", + "ui/src/pages/PluginSettings.tsx", + "ui/src/plugins/slots.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/plugins/plugin-contributions.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts", + "ui/storybook/support/capability-gaps/plugins.ts" + ], + "stableId": "feature-plugin-contributions", + "sidebarTitle": "Apps, channels, and extensions/Plugins/Contributed routes", + "fingerprint": "09b20d50e157c332613c3c03ccded5ae21804011005891cacd4342ce653b8d67" + }, + { + "file": "ui/storybook/stories/cases/case-browse.stories.tsx", + "capabilities": [ + "cases/browse" + ], + "purpose": "Automation and structured work/Cases/Browse and hierarchy", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Cases page with fixture parent/child records and loading/empty/error/403. Failed list queries currently use the shipped empty-case presentation. Responsive duplicate labels are accounted for; server filters and case mutations remain separate.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/cases.md", + "ui/src/pages/Cases.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/cases/case-browse.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts", + "ui/storybook/support/capability-gaps/cases.ts" + ], + "stableId": "feature-case-browse", + "sidebarTitle": "Automation and structured work/Cases/Browse and hierarchy", + "fingerprint": "41f3255e05cc2ca30fc22ff25748cff9b7017189f6ebb71d6f42827602237691" + }, + { + "file": "ui/storybook/stories/cases/case-detail.stories.tsx", + "capabilities": [ + "cases/structure", + "cases/history", + "cases/task-links" + ], + "purpose": "Automation and structured work/Cases/Structure, history and task links", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "CaseDetail hierarchy and activity, CaseFieldsPanel arbitrary/empty data and IssueCasesPanel reverse links. Fixture relations/events; document revision history and persisted links remain production obligations.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/cases.md", + "ui/src/components/CaseFieldsPanel.tsx", + "ui/src/components/IssueCasesPanel.tsx", + "ui/src/lib/router.tsx", + "ui/src/pages/CaseDetail.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/cases/case-detail.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts", + "ui/storybook/support/capability-gaps/cases.ts" + ], + "stableId": "feature-case-detail", + "sidebarTitle": "Automation and structured work/Cases/Structure, history and task links", + "fingerprint": "13a24fcf6de97812864194104d14343ec50130ba6aa5405078a1b127778ab0cf" + }, + { + "file": "ui/storybook/stories/status-cards/status-card-states.stories.tsx", + "capabilities": [ + "status-cards/authoring", + "status-cards/generation", + "status-cards/inspection", + "status-cards/archive" + ], + "purpose": "Automation and structured work/Status cards/Authoring, updates and archive", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "StatusCards board/drawer with current/stale/error/live setup, list states, create, refresh, inspection and archive/restoration. Mutable receipts and setup task ID are fixtures; no summarizer execution or revision-history parity is claimed.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/status-cards.md", + "ui/src/lib/router.tsx", + "ui/src/pages/StatusCards/ArchivedStatusCardRow.tsx", + "ui/src/pages/StatusCards/CreateStatusCardDialog.tsx", + "ui/src/pages/StatusCards/StatusCardDetailDrawer.tsx", + "ui/src/pages/StatusCards/StatusCardTile.tsx", + "ui/src/pages/StatusCards/index.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/status-cards/status-card-states.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts", + "ui/storybook/support/capability-gaps/status-cards.ts" + ], + "stableId": "feature-status-card-states", + "sidebarTitle": "Automation and structured work/Status cards/Authoring, updates and archive", + "fingerprint": "c6f52ca5e82fa4a14bc10b30177a0a9e689cd37d83fc5afbee77370f707bb13f" + }, + { + "file": "ui/storybook/stories/goals/goal-alignment.stories.tsx", + "capabilities": [ + "goals/alignment", + "goals/navigation" + ], + "purpose": "Projects and execution/Goals and work alignment/Aligned work and navigation", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "GoalDetail child navigation and aligned-project tab with empty/loading/failure/403. Current UI displays relationships but has no project goal-assignment control; no invented assignment UI.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/goals.md", + "ui/src/lib/router.tsx", + "ui/src/pages/GoalDetail.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/fixtures/paperclipData.ts", + "ui/storybook/stories/goals/goal-alignment.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-goal-alignment", + "sidebarTitle": "Projects and execution/Goals and work alignment/Aligned work and navigation", + "fingerprint": "a9a4bc0845dbed28f5cc458713f4ceecb3641a9ba1b60fe1867f3b573a471622" + }, + { + "file": "ui/storybook/stories/companies/company-import.stories.tsx", + "capabilities": [ + "companies/portability" + ], + "purpose": "Getting started and access/Companies and portability/Import preview", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Shipped CompanyImport GitHub-source preview, warnings, pending, error and 403. Plan/package replies are fictional; export, final import and real package fidelity remain separate scenarios/contracts. The merged production import page includes Codex harness/runner selection and preservation of native settings. This fixture review does not prove automatic native execution, credential reuse, import persistence or provider behavior.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/companies.md", + "ui/src/pages/CompanyImport.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/companies/company-import.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-company-import", + "sidebarTitle": "Getting started and access/Companies and portability/Import preview", + "fingerprint": "8cc9a61cc8224e050526635be9ac89e16496ecd15d575828e0a135baf0c6b82d" + }, + { + "file": "ui/storybook/stories/inbox-search/inbox-views.stories.tsx", + "capabilities": [ + "inbox-search/views" + ], + "purpose": "Tasks and conversations/Inbox, decisions, and search/Personal task views", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Shipped Inbox host with Mine/Recent/Unread/Blocked under combined-inbox flags and list states. Approvals/join requests/run lists are isolated alongside tasks. Failed/denied list queries currently fall back to Inbox zero; personal participation/read metadata and server counts remain production verification. Mine/recent fixture tasks are human-created; the upstream agent-created-task filter changes are separate visibility scenarios.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/inbox-search.md", + "ui/src/pages/Inbox.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/fixtures/paperclipData.ts", + "ui/storybook/stories/inbox-search/inbox-views.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-inbox-views", + "sidebarTitle": "Tasks and conversations/Inbox, decisions, and search/Personal task views", + "fingerprint": "6b0a316d05b6e02af3c50075086063ae9c748ea4edc0e67156235bcc1eb8e21b" + }, + { + "file": "ui/storybook/stories/skills/skill-test-runs.stories.tsx", + "capabilities": [ + "skills/testing" + ], + "purpose": "Agents and reusable capabilities/Skills and Skill Studio/Test runs", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "SkillStudio test history statuses and result detail with immutable fixture output/version. No agent/provider runs; authoring, harness execution, persisted costs and snapshots require production tests.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/skills.md", + "ui/src/lib/router.tsx", + "ui/src/pages/SkillStudio.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/skills/skill-test-runs.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts", + "ui/storybook/support/capability-gaps/skills.ts" + ], + "stableId": "feature-skill-test-runs", + "sidebarTitle": "Agents and reusable capabilities/Skills and Skill Studio/Test runs", + "fingerprint": "4685add5676a5e6dd96885d326be891b8b48520787177d8540d5f3e2758cb8c5" + }, + { + "file": "ui/storybook/stories/onboarding/resume-draft.stories.tsx", + "capabilities": [ + "onboarding/resume" + ], + "purpose": "Getting started and access/Onboarding and first work/Resume a saved draft", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "OnboardingWizard restores a seeded draft, malformed/unavailable-company handling and draft retention across a fixture remount. The reopen button belongs to the fixture host; real reload, account switch and storage failure remain separate checks. The merged wizard includes Codex harness/runner draft restoration and immutable-environment setup qualification. These isolated draft states do not establish the new native setup, task/follow-up, account or cleanup journey.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/onboarding.md", + "ui/src/api/companies-query.ts", + "ui/src/components/OnboardingWizard.tsx", + "ui/src/components/ui/button.tsx", + "ui/src/context/DialogContext.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/fixtures/onboardingDraft.ts", + "ui/storybook/stories/onboarding/resume-draft.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-onboarding-resume", + "sidebarTitle": "Getting started and access/Onboarding and first work/Resume a saved draft", + "fingerprint": "99d51dd447392e61a014351e956d3b93e36663ba7a795a591b694b05fe49e9ad" + }, + { + "file": "ui/storybook/stories/routines/run-now.stories.tsx", + "capabilities": [ + "routines/run-now" + ], + "purpose": "Automation and structured work/Routines, schedules, and triggers/Run now", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "RoutineDetail manual-run dialog with required input, captured variables, pending/error/success and read states. Fictional dispatch receipt; real scheduling, coalescing, workspace authority and execution remain separate.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/routines.md", + "ui/src/components/ToastViewport.tsx", + "ui/src/lib/router.tsx", + "ui/src/pages/RoutineDetail.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/fixtures/paperclipData.ts", + "ui/storybook/fixtures/routineWebhooks.tsx", + "ui/storybook/stories/routines/run-now.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-routine-run-now", + "sidebarTitle": "Automation and structured work/Routines, schedules, and triggers/Run now", + "fingerprint": "6d6f650f4990ad61045c6e3c47ae446cf5ac7a5240ca123a91c2c3216bf0d3c9" + }, + { + "file": "ui/storybook/stories/app-permissions/catalog-review.stories.tsx", + "capabilities": [ + "app-permissions/catalog-change" + ], + "purpose": "Apps, channels, and extensions/App access and action permissions/Review newly discovered actions", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Shipped ActionsSection quarantine reviewer with allowed/blocked decisions, empty/refreshing/saving/read-only props. Callbacks simulate catalog persistence; discovery failures belong to AppDetail and backend authority remains separate.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/app-permissions.md", + "ui/src/pages/apps/app-detail/PermissionsPanel.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/app-permissions/catalog-review.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-catalog-review", + "sidebarTitle": "Apps, channels, and extensions/App access and action permissions/Review newly discovered actions", + "fingerprint": "0b4c214f3c5e138fa57da3ed59a64c368d8ac7615476164743b810c117e7cab6" + }, + { + "file": "ui/storybook/stories/chat-channels/channel-attachments.stories.tsx", + "capabilities": [ + "chat-channels/attachments" + ], + "purpose": "Apps, channels, and extensions/External chat and email/Task-linked files", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "TaskChatBubble with received/outbound saved attachment receipts, no/multiple files and missing filename. File links are fictional; no download, provider transfer or authorization is proved.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/chat-channels.md", + "ui/src/components/task-chat/TaskChatBubble.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/chat-channels/channel-attachments.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-channel-attachments", + "sidebarTitle": "Apps, channels, and extensions/External chat and email/Task-linked files", + "fingerprint": "aa6718bf5754d2f5603682ed76d5075e277ce19f35b38356f92f8e3f14bc4f43" + }, + { + "file": "ui/storybook/stories/steering/composer-draft-recovery.stories.tsx", + "capabilities": [ + "steering/drafts" + ], + "purpose": "Tasks and conversations/Steering and queued messages/Draft recovery", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "TaskChatComposer uses owned task-keyed browser drafts, uncertain submission fence, switch/restore and failed/accepted send callbacks. API receipt authority and real submission durability remain separate. Storybook-owned draft keys are reset using the matching retained submission ID on setup and cleanup; same-browser uncertain-to-empty/send switching is covered separately.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/steering.md", + "ui/src/components/task-chat/TaskChatComposer.tsx", + "ui/src/components/ui/button.tsx", + "ui/src/lib/composer-draft.ts", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/steering/composer-draft-recovery.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-composer-draft-recovery", + "sidebarTitle": "Tasks and conversations/Steering and queued messages/Draft recovery", + "fingerprint": "1c242e44746a80add5d9d54ea46611fe58ec43aac42e9d8c61027495c7fa4279" + }, + { + "file": "ui/storybook/stories/inbox-search/personal-task-toasts.stories.tsx", + "capabilities": [ + "inbox-search/toasts" + ], + "purpose": "Tasks and conversations/Inbox, decisions, and search/Personal task notifications", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "LiveUpdatesProvider and ToastViewport consume fixture socket eligibility, suppress unrelated/visible-task events and deduplicate replay. Server eligibility metadata is simulated; no persisted multiplayer participation proof. The upstream Inbox recipe adds agent-created-task discovery checks; this LiveUpdatesProvider suite still tests toast eligibility only.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/inbox-search.md", + "ui/src/components/ToastViewport.tsx", + "ui/src/components/ui/button.tsx", + "ui/src/context/LiveUpdatesProvider.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/inbox-search/personal-task-toasts.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts", + "ui/storybook/support/capability-gaps/live-events.ts" + ], + "stableId": "feature-personal-task-toasts", + "sidebarTitle": "Tasks and conversations/Inbox, decisions, and search/Personal task notifications", + "fingerprint": "956b02e1631234b0eaba5622e416edec931a8151a2f6c4f069cdefee3c51a428" + }, + { + "file": "ui/storybook/stories/agent-chat/conference-room.stories.tsx", + "capabilities": [ + "agent-chat/conference-room" + ], + "purpose": "Tasks and conversations/Agent conversations and project handoff/Conference Room", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "BoardChat empty, reply, send pending/error/403 through fixture SSE and persisted comment reads. 403 uses the shipped generic unavailable message; route gates and real Claude execution remain separate.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/agent-chat.md", + "ui/src/pages/BoardChat.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/fixtures/paperclipData.ts", + "ui/storybook/stories/agent-chat/conference-room.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-conference-room", + "sidebarTitle": "Tasks and conversations/Agent conversations and project handoff/Conference Room", + "fingerprint": "2c2431d14a8e6106a6ede10021ea387b7b5e8a9770710f423cbe95ba316dc4da" + }, + { + "file": "ui/storybook/stories/documents-artifacts/document-revisions.stories.tsx", + "capabilities": [ + "documents-artifacts/documents" + ], + "purpose": "Tasks and conversations/Documents, attachments, and work products/Document revisions", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "IssueDocumentsSection renders current/edit/historical preview and append-only restore with mutation error. Read/loading failures retain shipped fallback rendering; no persisted concurrency or authorization proof.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/documents-artifacts.md", + "ui/src/components/DocumentFrameHeader.tsx", + "ui/src/components/IssueDocumentsSection.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/fixtures/paperclipData.ts", + "ui/storybook/stories/documents-artifacts/document-revisions.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-document-revisions", + "sidebarTitle": "Tasks and conversations/Documents, attachments, and work products/Document revisions", + "fingerprint": "a1d9e6b7f17a5d75ac2ff91a0ad96b9de9dadab4b2e0340ac7ae94d32809409b" + }, + { + "file": "ui/storybook/stories/navigation-preferences/sidebar-preferences.stories.tsx", + "capabilities": [ + "navigation-preferences/preferences" + ], + "purpose": "Oversight and operation/Navigation, profile, and announcements/Sidebar preferences", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Production Sidebar inside PluginLauncherProvider with saved-order reads, pending/error/403 fallback and session-only section collapse. Favorites, recents and actual order persistence still need the recipe checks.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/navigation-preferences.md", + "ui/src/components/Sidebar.tsx", + "ui/src/components/SidebarSection.tsx", + "ui/src/plugins/launchers.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/navigation-preferences/sidebar-preferences.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts" + ], + "stableId": "feature-sidebar-preferences", + "sidebarTitle": "Oversight and operation/Navigation, profile, and announcements/Sidebar preferences", + "fingerprint": "5426596737a1f27105954d0e7fde6729f0b3a2e83ba7dec9e288286a359ab216" + }, + { + "file": "ui/storybook/stories/pipelines/pipeline-board.stories.tsx", + "capabilities": [ + "pipelines/items" + ], + "purpose": "Automation and structured work/Pipelines, review queues, and learnings/Items", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Production Pipelines board with stages/items and loading/empty/error/403. Snapshot rows only; transitions, liveness, guards and automation execution remain separate.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/pipelines.md", + "ui/src/lib/router.tsx", + "ui/src/pages/Pipelines.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/pipelines/pipeline-board.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts", + "ui/storybook/support/capability-gaps/pipelines.ts" + ], + "stableId": "feature-pipeline-board", + "sidebarTitle": "Automation and structured work/Pipelines, review queues, and learnings/Items", + "fingerprint": "6259bfc636e56d936fa77164bd5acbc5967066721a37f75bc803f5a4db6b3be3" + }, + { + "file": "ui/storybook/stories/pipelines/stage-settings.stories.tsx", + "capabilities": [ + "pipelines/stages" + ], + "purpose": "Automation and structured work/Pipelines, review queues, and learnings/Stage settings", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Production PipelineSettings stage editor with filled configuration, no stages, pending/error/403. This pass renders the shipped controls; saved stage/automation changes remain production obligations.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/pipelines.md", + "ui/src/lib/router.tsx", + "ui/src/pages/PipelineSettings.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/pipelines/stage-settings.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts", + "ui/storybook/support/capability-gaps/pipelines.ts" + ], + "stableId": "feature-pipeline-stages", + "sidebarTitle": "Automation and structured work/Pipelines, review queues, and learnings/Stage settings", + "fingerprint": "747e8cf58dd88118466de6b4ba8967f2f76ed31f7a8ee300f05265630367f0aa" + }, + { + "file": "ui/storybook/stories/pipelines/review-queue.stories.tsx", + "capabilities": [ + "pipelines/review" + ], + "purpose": "Automation and structured work/Pipelines, review queues, and learnings/Review queue", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Production ReviewQueue with empty/loading/error/403, approval removal and failure recovery marked Needs attention. Fixture decisions do not prove real review leases, version checks or transitions.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/pipelines.md", + "ui/src/lib/router.tsx", + "ui/src/pages/Pipelines.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/pipelines/review-queue.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts", + "ui/storybook/support/capability-gaps/pipelines.ts" + ], + "stableId": "feature-pipeline-review", + "sidebarTitle": "Automation and structured work/Pipelines, review queues, and learnings/Review queue", + "fingerprint": "360dedf71d3540f1b67880ba49687a3b6a8962a11fdc8c3a2acd375c695a883c" + }, + { + "file": "ui/storybook/stories/pipelines/learnings.stories.tsx", + "capabilities": [ + "pipelines/learnings" + ], + "purpose": "Automation and structured work/Pipelines, review queues, and learnings/Learnings", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "Production Learnings feed renders a review_decided event plus loading/empty/error/403. Event provenance and durable audit records remain separate; this is not a new learning-record subsystem.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/pipelines.md", + "ui/src/lib/router.tsx", + "ui/src/pages/Pipelines.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/pipelines/learnings.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts", + "ui/storybook/support/capability-gaps/pipelines.ts" + ], + "stableId": "feature-pipeline-learnings", + "sidebarTitle": "Automation and structured work/Pipelines, review queues, and learnings/Learnings", + "fingerprint": "acc796cc3ca32601e40809ffb0b5e1406c1360785c988d74baca3172cad71b49" + }, + { + "file": "ui/storybook/stories/execution-environments/environment-states.stories.tsx", + "capabilities": [ + "execution-environments/configuration", + "execution-environments/capabilities", + "execution-environments/probe" + ], + "purpose": "Projects and execution/Execution environments/Configuration, capabilities, and probes", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "CompanyEnvironments configuration/edit, unsupported capabilities and probe pending/failure/success. No provider contacted. List read/loading failures expose shipped fallback behavior, not dedicated error UI.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/execution-environments.md", + "ui/src/components/ToastViewport.tsx", + "ui/src/lib/router.tsx", + "ui/src/pages/CompanyEnvironments.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/execution-environments/environment-states.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts", + "ui/storybook/support/capability-gaps/environments.ts" + ], + "stableId": "feature-execution-environments", + "sidebarTitle": "Projects and execution/Execution environments/Configuration, capabilities, and probes", + "fingerprint": "9a14e48be15b72210745d53541505a92e331437b67369d42d6364e78938b5120" + }, + { + "file": "ui/storybook/stories/execution-environments/custom-images.stories.tsx", + "capabilities": [ + "execution-environments/images" + ], + "purpose": "Projects and execution/Execution environments/Custom images", + "status": "current", + "implementation": "production", + "boundary": "fixtures", + "notes": "CompanyEnvironments image metadata: unconfigured, captured, unsupported, failed, loading and 403. No live setup terminal, provider capture, rollback or relink is executed.", + "reviewedAt": "2026-10-13", + "sources": [ + "maps/feature-map/execution-environments.md", + "ui/src/components/ToastViewport.tsx", + "ui/src/lib/router.tsx", + "ui/src/pages/CompanyEnvironments.tsx", + "ui/storybook/.storybook/preview.tsx", + "ui/storybook/stories/execution-environments/custom-images.stories.tsx", + "ui/storybook/support/capability-gaps/api.ts", + "ui/storybook/support/capability-gaps/environments.ts" + ], + "stableId": "feature-environment-images", + "sidebarTitle": "Projects and execution/Execution environments/Custom images", + "fingerprint": "0877c994c37d7272b9a1330897cf56d366162dc17e7503c753415dd1077c9446" } ] } diff --git a/tests/storybook-visual/feature-gap-render-states.ts b/tests/storybook-visual/feature-gap-render-states.ts new file mode 100644 index 00000000000..4180eef61c4 --- /dev/null +++ b/tests/storybook-visual/feature-gap-render-states.ts @@ -0,0 +1,321 @@ +import { expect, type FrameLocator } from "@playwright/test"; + +// These expectations name shipped UI states, not captured visual baselines. +// Explicit fallback cases document where production has no separate error UI. +type State = { + text?: string | RegExp; + absent?: string | RegExp; + skeleton?: true; + selector?: string; + emptyRootReason?: string; + tokenHistory?: "active" | "expired" | "empty"; + editable?: true; + defaultEnvironment?: true; +}; +const failed: State = { text: "The request failed. Please try again." }; +const denied: State = { + text: "You do not have permission to perform this action.", +}; +const skeleton: State = { skeleton: true }; +const documentFallback: State = { + text: "New document", + absent: "Run the release checks.", +}; +const environmentFallback: State = { + defaultEnvironment: true, + absent: "Release sandbox", +}; +const states: Record> = { + "feature-cli-authorization": { + pending: { text: "Approve Paperclip CLI access" }, + loading: { text: "Loading CLI auth challenge..." }, + expired: { text: "CLI auth challenge expired" }, + unavailable: { text: "CLI auth challenge unavailable" }, + }, + "feature-membership": { + members: { text: "Release reviewer" }, + loading: { text: "Loading organization access…" }, + empty: { text: "No user memberships found for this organization yet." }, + failed, + "permission-denied": { + text: "You do not have permission to manage organization members.", + }, + }, + "feature-sign-in": { + "signed-out": { text: "Sign in to Paperclip" }, + loading: { text: "Loading…" }, + "session-failed": { + text: "Unable to check sign-in. Refresh and try again.", + }, + }, + "feature-run-audit": { + loading: { text: "Loading runs…" }, + empty: { text: "No runs yet." }, + }, + "feature-conference-room": { + empty: { text: "Conference Room", editable: true }, + }, + "feature-catalog-review": { + quarantined: { text: "Review 1 new action" }, + empty: { text: "No actions match “”. Clear the search to see them all." }, + refreshing: { text: "Review 1 new action", selector: ".animate-spin" }, + saving: { text: "Saving…" }, + "read-only": { text: "Actions", absent: "Review 1 new action" }, + }, + "feature-budget-resolution": { + loading: skeleton, + empty: { text: /No budget policies yet\./ }, + "load-failed": { + text: "Budget data could not be loaded. Please try again shortly.", + }, + "permission-denied": { + text: "Budget data could not be loaded. Please try again shortly.", + }, + }, + // Cases currently uses its empty presentation after a failed list query. + "feature-case-browse": { + loading: skeleton, + empty: { text: "No cases yet" }, + failed: { text: "No cases yet" }, + "permission-denied": { text: "No cases yet" }, + }, + "feature-case-detail": { + loading: skeleton, + failed: { text: "Case not found." }, + "permission-denied": { text: "Case not found." }, + "arbitrary-fields": { text: "0.3.1" }, + }, + "feature-channel-attachments": { + "no-files": { text: "Here is the release report.", absent: /Files ·/ }, + "multiple-files": { text: "Files · 2" }, + "missing-filename": { text: "attachment" }, + }, + "feature-company-import": { "choose-source": { text: "Import source" } }, + "feature-company-selection": { + organizations: { text: "Archived Research" }, + loading: { text: "Loading organizations..." }, + empty: { text: "New Organization", absent: "Paperclip Storybook" }, + failed, + "permission-denied": { + text: "New Organization", + absent: "Paperclip Storybook", + }, + }, + "feature-document-revisions": { + empty: documentFallback, + loading: documentFallback, + "read-failed": documentFallback, + "permission-denied": documentFallback, + edit: { text: "Run the release checks.", editable: true }, + }, + "feature-environment-images": { + "no-image": { text: "Not configured" }, + "image-read-failed": { text: "Image metadata is unavailable." }, + "unsupported-provider": { text: "Unsupported provider" }, + }, + "feature-execution-environments": { + loading: environmentFallback, + empty: environmentFallback, + failed: environmentFallback, + "permission-denied": environmentFallback, + "unsupported-provider": { text: "Isolated release checks" }, + "edit-configuration": { + text: "Edit environment", + selector: '[data-testid="environment-form-page"]', + }, + }, + "feature-gateway-tokens": { + "active-token": { tokenHistory: "active" }, + "no-tokens": { tokenHistory: "empty" }, + "expired-token": { tokenHistory: "expired" }, + }, + "feature-goal-alignment": { + "empty-relationships": { text: "No sub-goals." }, + loading: skeleton, + failed, + "permission-denied": denied, + }, + "feature-inbox-views": { + mine: { text: "Create super-detailed storybooks for the project" }, + recent: { text: "Create super-detailed storybooks for the project" }, + unread: { text: "Create super-detailed storybooks for the project" }, + blocked: { text: "No work is stopped." }, + empty: { text: "Inbox zero." }, + loading: skeleton, + failed: { text: "Inbox zero." }, + "permission-denied": { text: "Inbox zero." }, + }, + // Order reads are allowed to fall back to navigation; section collapse is local. + "feature-sidebar-preferences": { + "saved-order": { + text: "Paperclip Storybook", + selector: 'a[href="/PAP/dashboard"]', + }, + "loading-order": { + text: "Paperclip Storybook", + selector: 'a[href="/PAP/dashboard"]', + }, + "order-read-failed": { + text: "Paperclip Storybook", + selector: 'a[href="/PAP/dashboard"]', + }, + "order-permission-denied": { + text: "Paperclip Storybook", + selector: 'a[href="/PAP/dashboard"]', + }, + }, + "feature-onboarding-resume": { + "malformed-draft": { + text: "Create your first agent", + selector: '[data-testid="onboarding-wizard"]', + }, + }, + "feature-pipeline-learnings": { + loading: skeleton, + empty: { text: "No learnings yet." }, + failed: { text: "Could not load learnings." }, + "permission-denied": { text: "Could not load learnings." }, + }, + "feature-pipeline-board": { + loading: skeleton, + empty: { text: "0 total items", absent: "Ship the release notes" }, + failed: { text: "Pipeline not found." }, + "permission-denied": { text: "Pipeline not found." }, + }, + "feature-pipeline-review": { + loading: skeleton, + empty: { text: "Nothing needs you right now." }, + failed: { text: "Some items need attention. Try again in a moment." }, + "permission-denied": { + text: "Some items need attention. Try again in a moment.", + }, + }, + "feature-pipeline-stages": { + loading: skeleton, + failed, + "permission-denied": denied, + }, + "feature-plugin-configuration": { + configured: { + text: "Preview plugin", + selector: 'input[value="Release preview"]', + }, + loading: { text: "Loading plugin details..." }, + unavailable: { text: "Failed to load plugins." }, + }, + "feature-plugin-contributions": { + loading: { text: "Loading…" }, + "missing-contribution": { text: "Preview plugin" }, + "discovery-failure": { text: "Loading…" }, + }, + "feature-plugin-management": { + installed: { text: "Preview plugin" }, + empty: { text: "No plugins installed" }, + loading: { text: "Loading plugins..." }, + failed: { text: "Failed to load plugins." }, + "permission-denied": { text: "Failed to load plugins." }, + }, + "feature-routine-run-now": { + overview: { text: "Review a release" }, + loading: skeleton, + failed, + "permission-denied": denied, + }, + "feature-secret-proposal-review": { + pending: { text: "dev/github/token" }, + loading: { text: "Loading proposals…" }, + empty: { text: "No pending proposals" }, + failed: { text: "Couldn’t load proposals. Try again." }, + }, + "feature-skill-test-runs": { + "completed-test": { text: "succeeded" }, + queued: { text: "queued" }, + running: { text: "running" }, + "test-failed": { text: "failed" }, + empty: { text: "No test runs yet. Pick an agent and Run." }, + loading: { text: "Loading skill…" }, + "load-failed": { text: "Skill not found." }, + "permission-denied": { text: "Skill not found." }, + }, + "feature-status-card-states": { + loading: { text: "Loading cards…" }, + empty: { text: "No status cards yet" }, + "load-failed": { text: "Could not load status cards" }, + "permission-denied": { + text: "You do not have permission to perform this action.", + }, + "setting-up": { text: "Setting up your card…" }, + "generation-failed": { text: "Last update failed" }, + "stale-summary": { text: "3 changes since last update" }, + }, + "feature-composer-draft-recovery": { + restored: { text: "Review the release before publishing.", editable: true }, + empty: { editable: true, absent: "Review the release before publishing." }, + "uncertain-submission": { + text: /We couldn’t confirm whether this comment was saved\./, + selector: '[role="alert"]', + }, + }, + "feature-durable-response-queue": { + empty: { + emptyRootReason: + "The shipped queued-message component returns null for zero entries.", + }, + }, +}; +export async function assertRenderState(frame: FrameLocator, id: string) { + const [suite, variant] = id.split("--"); + const state = states[suite]?.[variant]; + if (!state) + throw new Error(`Declare an explicit render expectation for ${id}`); + expect( + state, + `Declare an explicit render expectation for ${id}`, + ).toBeDefined(); + if (state.emptyRootReason) { + await expect(frame.locator("#storybook-root")).toBeEmpty(); + return; + } + if (state.text) + await expect( + frame + .getByText(state.text, { exact: typeof state.text === "string" }) + .first(), + ).toBeVisible(); + if (state.absent) + await expect( + frame.getByText(state.absent, { + exact: typeof state.absent === "string", + }), + ).toHaveCount(0); + if (state.skeleton) + await expect(frame.locator('[data-slot="skeleton"]').first()).toBeVisible(); + if (state.selector) + await expect(frame.locator(state.selector).first()).toBeVisible(); + if (state.defaultEnvironment) { + const select = frame.getByRole("combobox", { name: "Default environment" }); + await expect(select).toBeVisible(); + await expect(select).toHaveValue(""); + await expect( + select.getByRole("option", { name: "Local", exact: true }), + ).toBeAttached(); + } + if (state.editable) + await expect(frame.getByRole("textbox").first()).toBeVisible(); + if (state.tokenHistory) { + await frame.getByRole("button", { name: /Token history/ }).click(); + if (state.tokenHistory === "empty") + await expect( + frame.getByText("No tokens have been issued for this gateway."), + ).toBeVisible(); + else { + await expect( + frame.getByText("Preview client", { exact: true }).first(), + ).toBeVisible(); + if (state.tokenHistory === "expired") + await expect( + frame.getByText("Expired", { exact: true }).first(), + ).toBeVisible(); + } + } +} diff --git a/tests/storybook-visual/feature-gap-stories.config.ts b/tests/storybook-visual/feature-gap-stories.config.ts new file mode 100644 index 00000000000..97fa97230e4 --- /dev/null +++ b/tests/storybook-visual/feature-gap-stories.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "@playwright/test"; +export default defineConfig({ + testDir: ".", + testMatch: "feature-gap-stories.spec.ts", + timeout: 60_000, + expect: { timeout: 15_000 }, + retries: 0, + fullyParallel: true, + workers: 4, + reporter: "list", + outputDir: "./test-results/feature-gaps", + use: { + baseURL: process.env.PAPERCLIP_GAP_STORYBOOK_URL ?? "http://localhost:6136", + viewport: { width: 1280, height: 900 }, + reducedMotion: "reduce", + }, +}); diff --git a/tests/storybook-visual/feature-gap-stories.spec.ts b/tests/storybook-visual/feature-gap-stories.spec.ts new file mode 100644 index 00000000000..e9fd51e7072 --- /dev/null +++ b/tests/storybook-visual/feature-gap-stories.spec.ts @@ -0,0 +1,170 @@ +import { assertRenderState } from "./feature-gap-render-states"; +import { readFileSync, writeFileSync } from "node:fs"; +import { expect, test } from "@playwright/test"; +import { scanStories } from "../../scripts/storybook-catalog.mjs"; +const root = new URL("../../", import.meta.url).pathname; +const catalog = JSON.parse( + readFileSync( + new URL("../../maps/storybook-map/catalog.json", import.meta.url), + "utf8", + ), +); +const suites = catalog.entries.filter( + (entry: { stableId: string }) => + entry.stableId.startsWith("feature-") && + !entry.stableId.startsWith("feature-map-"), +); +const parsed = scanStories(root); +let index: { entries: Record }; +test.beforeAll(async ({ request }) => { + const response = await request.get("/index.json"); + expect(response.ok()).toBe(true); + index = await response.json(); +}); +test.afterEach(async ({ page }, info) => { + if (info.status !== info.expectedStatus) { + const managerPath = info.outputPath("manager.txt"); + writeFileSync(managerPath, await page.locator("body").innerText()); + await info.attach("storybook-page", { + path: managerPath, + contentType: "text/plain", + }); + const frame = page.frameLocator("#storybook-preview-iframe"); + const subjectPath = info.outputPath("subject.txt"); + writeFileSync(subjectPath, await frame.locator("body").innerText()); + await info.attach("storybook-subject", { + path: subjectPath, + contentType: "text/plain", + }); + } +}); +for (const suite of suites) { + const file = parsed.find( + (story: { file: string }) => story.file === suite.file, + ); + for (const id of file.ids) { + test(id, async ({ page }, info) => { + expect( + index.entries[id], + `Rebuild Storybook to include ${id}`, + ).toBeDefined(); + const errors: string[] = []; + page.on("pageerror", (error) => errors.push(error.message)); + // Opening an addon during play steals focus from inputs and dismisses menus. + await page.goto( + `/?path=/story/${id}&addonPanel=storybook/interactions/panel`, + ); + const frame = page.frameLocator("#storybook-preview-iframe"); + await expect(frame.locator("body")).toHaveClass(/sb-show-main/); + await expect(frame.locator(".sb-show-errordisplay")).toHaveCount(0); + await expect( + page.getByRole("tab", { name: /^Interactions/ }), + ).toHaveAttribute("aria-selected", "true"); + const status = page.locator('[aria-label^="Story status:"]'); + if (index.entries[id]?.tags.includes("play-fn")) { + await expect(status).toHaveAttribute( + "aria-label", + "Story status: Pass", + ); + } + if (!index.entries[id].tags.includes("play-fn")) + await assertRenderState(frame, id); + await expect(frame.locator("#storybook-root")).toBeAttached(); + expect(errors).toEqual([]); + if ( + id.includes("accepted") || + id.includes("permission") || + id.includes("save-retention") + ) + await page.screenshot({ path: info.outputPath(`${id}.png`) }); + }); + } +} + +test("token fixtures reset when switching stories in one browser", async ({ + page, +}) => { + await page.goto( + "/?path=/story/feature-gateway-tokens--issued-token&addonPanel=storybook/interactions/panel", + ); + await expect( + page.getByRole("tab", { name: /^Interactions/ }), + ).toHaveAttribute("aria-selected", "true"); + await expect(page.locator('[aria-label^="Story status:"]')).toHaveAttribute( + "aria-label", + "Story status: Pass", + ); + await page.getByRole("link", { name: "Active Token", exact: true }).click(); + const frame = page.frameLocator("#storybook-preview-iframe"); + await frame.getByRole("button", { name: /Token history/ }).click(); + await expect( + frame.getByText("Preview client", { exact: true }).first(), + ).toBeVisible(); + await expect(frame.getByText("Review client", { exact: true })).toHaveCount( + 0, + ); +}); + +test("draft fixtures reset when switching away from uncertain submission", async ({ + page, +}) => { + await page.goto( + "/?path=/story/feature-composer-draft-recovery--uncertain-submission&addonPanel=storybook/interactions/panel", + ); + const frame = page.frameLocator("#storybook-preview-iframe"); + const warning = frame.getByRole("alert").filter({ + hasText: "We couldn’t confirm whether this comment was saved.", + }); + const editor = frame.getByRole("textbox"); + await expect(warning).toContainText( + "We couldn’t confirm whether this comment was saved.", + ); + await page.getByRole("link", { name: "Empty", exact: true }).click(); + await expect(warning).toHaveCount(0); + await expect(editor).toHaveText(""); + for (const story of ["Sent Clears Draft", "Send Failed Keeps Draft"]) { + await page + .getByRole("link", { name: "Uncertain Submission", exact: true }) + .click(); + await expect(warning).toContainText( + "We couldn’t confirm whether this comment was saved.", + ); + // Keep the browser/storage context, but reload for play stories so the + // manager cannot report the previous no-play story's Pass during navigation. + const id = + story === "Sent Clears Draft" + ? "sent-clears-draft" + : "send-failed-keeps-draft"; + await page.goto( + `/?path=/story/feature-composer-draft-recovery--${id}&addonPanel=storybook/interactions/panel`, + ); + await expect(frame.locator("body")).toHaveClass(/sb-show-main/); + await expect(editor).toBeVisible(); + await expect(warning).toHaveCount(0); + await expect(page.locator('[aria-label^="Story status:"]')).toHaveAttribute( + "aria-label", + "Story status: Pass", + ); + await expect(editor).toHaveText( + story === "Sent Clears Draft" + ? "" + : "Review the release before publishing.", + ); + } +}); + +for (const theme of ["light", "dark"]) { + test(`backup retention renders on mobile in ${theme}`, async ({ + page, + }, info) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto( + `/iframe.html?id=feature-backup-retention--current-policy&viewMode=story&globals=theme:${theme}`, + ); + await expect(page.getByRole("button", { name: "14 days" })).toBeVisible(); + await expect(page.locator(".sb-show-errordisplay")).toHaveCount(0); + await page.screenshot({ + path: info.outputPath(`backup-mobile-${theme}.png`), + }); + }); +} diff --git a/ui/storybook/.storybook/preview.tsx b/ui/storybook/.storybook/preview.tsx index ecdfd5fdda9..8b90a8116af 100644 --- a/ui/storybook/.storybook/preview.tsx +++ b/ui/storybook/.storybook/preview.tsx @@ -1,3 +1,4 @@ +import { activatePreparedFixtures } from "../support/capability-gaps/api"; import { useLayoutEffect, useState, type ReactNode } from "react"; import type { Preview } from "@storybook/react-vite"; import { MINIMAL_VIEWPORTS } from "storybook/viewport"; @@ -764,6 +765,7 @@ function StorybookProviders({ if (typeof window !== "undefined") { installStorybookApiFixtures(); + activatePreparedFixtures(); } const [viewportReady, setViewportReady] = useState(() => !initialViewportWidth || window.parent === window || window.innerWidth === initialViewportWidth); diff --git a/ui/storybook/stories/access/cli-authorization.stories.tsx b/ui/storybook/stories/access/cli-authorization.stories.tsx new file mode 100644 index 00000000000..110d441e0e7 --- /dev/null +++ b/ui/storybook/stories/access/cli-authorization.stories.tsx @@ -0,0 +1,125 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Routes, Route } from "@/lib/router"; +import { CliAuthPage } from "@/pages/CliAuth"; +import { expect, userEvent, within } from "storybook/test"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +function CliAuthorization() { + return ( + + } /> + + ); +} +const meta = { + id: "feature-cli-authorization", + title: + "Getting started and access/Login, invitations, and access/CLI authorization", + component: CliAuthorization, + parameters: { + initialEntries: ["/cli-auth/challenge-gap?token=fixture-code"], + layout: "padded", + }, + beforeEach: ({ parameters }) => { + let status = parameters.status ?? "pending"; + return prepareFixtureApi( + [ + { + path: "/api/cli-auth/challenges/challenge-gap", + read: () => ({ + id: "challenge-gap", + status, + command: "paperclipai auth login", + clientName: "Review workstation", + requestedAccess: "board", + requestedCompanyId: "company-storybook", + requestedCompanyName: "Acme Research", + approvedAt: null, + expiresAt: "2099-01-01T00:00:00Z", + requiresSignIn: false, + canApprove: !parameters.blocked, + }), + }, + { + path: "/api/cli-auth/challenges/challenge-gap/approve", + method: "POST", + read: () => { + status = "approved"; + return { + approved: true, + status, + userId: "user-storybook", + keyId: "fixture-key", + expiresAt: "2099-01-01T00:00:00Z", + }; + }, + }, + { + path: "/api/cli-auth/challenges/challenge-gap/cancel", + method: "POST", + read: () => { + status = "cancelled"; + return { cancelled: true, status }; + }, + }, + ], + parameters.fixtureState as FixtureState, + ).cleanup; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Pending: Story = {}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Expired: Story = { parameters: { status: "expired" } }; +export const Unavailable: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionBlocked: Story = { + parameters: { blocked: true }, + play: async ({ canvasElement }) => { + await expect( + await within(canvasElement).findByRole("button", { + name: "Approve CLI access", + }), + ).toBeDisabled(); + }, +}; +export const Approved: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click( + await c.findByRole("button", { name: "Approve CLI access" }), + ); + await c.findByRole("heading", { name: "CLI access approved" }); + }, +}; +export const Approving: Story = { + parameters: { fixtureState: "saving" }, + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click( + await c.findByRole("button", { name: "Approve CLI access" }), + ); + await expect( + await c.findByRole("button", { name: "Approving..." }), + ).toBeDisabled(); + }, +}; +export const ApprovalFailed: Story = { + parameters: { fixtureState: "mutation-failure" }, + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click( + await c.findByRole("button", { name: "Approve CLI access" }), + ); + await c.findByText("The request failed. Please try again."); + }, +}; +export const Cancelled: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click(await c.findByRole("button", { name: "Cancel" })); + await c.findByRole("heading", { name: "CLI auth challenge cancelled" }); + }, +}; diff --git a/ui/storybook/stories/access/membership.stories.tsx b/ui/storybook/stories/access/membership.stories.tsx new file mode 100644 index 00000000000..661f119e917 --- /dev/null +++ b/ui/storybook/stories/access/membership.stories.tsx @@ -0,0 +1,113 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { ToastViewport } from "@/components/ToastViewport"; +import { CompanyAccess } from "@/pages/CompanyAccess"; +import { expect, userEvent, within, waitFor } from "storybook/test"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +let member = { + id: "member-gap", + companyId: "company-storybook", + principalType: "user", + principalId: "user-review", + status: "active", + membershipRole: "operator", + user: { + id: "user-review", + email: "reviewer@example.test", + name: "Release reviewer", + image: null, + }, + grants: [], + createdAt: "2026-10-12T12:00:00Z", + updatedAt: "2026-10-12T12:00:00Z", +}; +function MembershipHost() { + return ( + <> + + + + ); +} +const meta = { + id: "feature-membership", + title: + "Getting started and access/Login, invitations, and access/Organization membership", + component: MembershipHost, + parameters: { layout: "padded" }, + beforeEach: ({ parameters }) => { + member = { ...member, membershipRole: "operator", status: "active" }; + return prepareFixtureApi( + [ + { + path: "/api/companies/company-storybook/members", + read: () => ({ + members: parameters.fixtureState === "empty" ? [] : [member], + access: { + currentUserRole: "owner", + canManageMembers: true, + canInviteUsers: true, + canApproveJoinRequests: true, + }, + }), + }, + { + path: "/api/companies/company-storybook/join-requests", + read: () => [], + }, + { + path: "/api/companies/company-storybook/members/member-gap", + method: "PATCH", + read: ({ body }) => { + member = { ...member, ...body }; + return member; + }, + }, + ], + parameters.fixtureState as FixtureState, + ).cleanup; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Members: Story = {}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Empty: Story = { parameters: { fixtureState: "empty" } }; +export const Failed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; +export const UpdateRole: Story = { + play: async ({ canvasElement }) => { + await userEvent.click( + await within(canvasElement).findByRole("button", { name: "Edit" }), + ); + const c = within(document.body); + await userEvent.selectOptions( + c.getByRole("combobox", { name: "Organization role" }), + "viewer", + ); + await userEvent.click(c.getByRole("button", { name: "Save member" })); + await waitFor(() => expect(member.membershipRole).toBe("viewer")); + await waitFor(() => + expect(c.queryByRole("dialog")).not.toBeInTheDocument(), + ); + }, +}; +export const SaveFailed: Story = { + parameters: { fixtureState: "mutation-failure" }, + play: async ({ canvasElement }) => { + await userEvent.click( + await within(canvasElement).findByRole("button", { name: "Edit" }), + ); + const c = within(document.body); + await userEvent.selectOptions( + c.getByRole("combobox", { name: "Organization role" }), + "viewer", + ); + await userEvent.click(c.getByRole("button", { name: "Save member" })); + await c.findByText("The request failed. Please try again."); + }, +}; diff --git a/ui/storybook/stories/access/sign-in.stories.tsx b/ui/storybook/stories/access/sign-in.stories.tsx new file mode 100644 index 00000000000..8bab9eb7b0b --- /dev/null +++ b/ui/storybook/stories/access/sign-in.stories.tsx @@ -0,0 +1,107 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Routes, Route } from "@/lib/router"; +import { AuthPage } from "@/pages/Auth"; +import { Companies } from "@/pages/Companies"; +import { expect, userEvent, within, waitFor } from "storybook/test"; +import { storybookAuthSession } from "../../fixtures/paperclipData"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +function SignIn() { + return ( + + } /> + } /> + } /> + + ); +} +const meta = { + id: "feature-sign-in", + title: "Getting started and access/Login, invitations, and access/Sign in", + component: SignIn, + parameters: { + layout: "fullscreen", + initialEntries: ["/auth?next=/companies"], + }, + beforeEach: ({ parameters }) => { + let session: unknown = null; + return prepareFixtureApi( + [ + { + path: "/api/health", + state: "ready", + read: () => ({ + status: "ok", + deploymentMode: "authenticated", + authReady: true, + }), + }, + { path: "/api/auth/get-session", read: () => session }, + { + path: "/api/auth/sign-in/email", + method: "POST", + read: () => { + session = storybookAuthSession; + return { user: storybookAuthSession.user, token: "fixture-only" }; + }, + }, + ], + parameters.fixtureState as FixtureState, + ).cleanup; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const SignedOut: Story = {}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const SessionFailed: Story = { parameters: { fixtureState: "failure" } }; +async function submit(canvasElement: HTMLElement) { + const c = within(canvasElement); + const email = await c.findByRole("textbox", { name: "Email" }); + await userEvent.clear(email); + await userEvent.click(email); + await userEvent.paste("reviewer@example.test"); + const password = c.getByLabelText("Password"); + await userEvent.clear(password); + await userEvent.click(password); + await userEvent.paste("fixture-password"); + await waitFor(() => { + expect(c.getByRole("textbox", { name: "Email" })).toHaveValue( + "reviewer@example.test", + ); + expect(c.getByLabelText("Password")).toHaveValue("fixture-password"); + expect(c.getByRole("button", { name: "Sign In" })).toHaveAttribute( + "aria-disabled", + "false", + ); + }); + await userEvent.click(c.getByRole("button", { name: "Sign In" })); +} +export const SignInFailed: Story = { + parameters: { fixtureState: "mutation-failure" }, + play: async ({ canvasElement }) => { + await submit(canvasElement); + await within(canvasElement).findByText( + "The request failed. Please try again.", + ); + }, +}; +export const SigningIn: Story = { + parameters: { fixtureState: "saving" }, + play: async ({ canvasElement }) => { + await submit(canvasElement); + await expect( + await within(canvasElement).findByRole("button", { name: "Working…" }), + ).toBeDisabled(); + }, +}; +export const SignedIn: Story = { + play: async ({ canvasElement }) => { + await submit(canvasElement); + await within(canvasElement).findByRole("button", { + name: "New Organization", + }); + }, +}; diff --git a/ui/storybook/stories/activity/run-audit.stories.tsx b/ui/storybook/stories/activity/run-audit.stories.tsx new file mode 100644 index 00000000000..942744a096c --- /dev/null +++ b/ui/storybook/stories/activity/run-audit.stories.tsx @@ -0,0 +1,85 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, within } from "storybook/test"; +import { AuditRuns } from "@/pages/audit/AuditRuns"; +import { + storybookLiveRuns, + storybookAgents, +} from "../../fixtures/paperclipData"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +const meta = { + id: "feature-run-audit", + title: + "Oversight and operation/Dashboards and audit trails/Execution history", + component: AuditRuns, + args: { companyId: "company-storybook" }, + parameters: { layout: "padded" }, + beforeEach: ({ parameters }) => + prepareFixtureApi( + [ + { + path: "/api/companies/company-storybook/heartbeat-runs", + read: () => + parameters.fixtureState === "empty" + ? [] + : [ + { + ...storybookLiveRuns[0], + id: "run-gap", + agentId: storybookAgents[0].id, + status: "succeeded", + invocationSource: "on_demand", + startedAt: "2026-10-12T12:00:00Z", + finishedAt: "2026-10-12T12:01:00Z", + resultJson: { summary: "Reviewed the release checklist" }, + }, + ], + }, + ], + parameters.fixtureState as FixtureState, + ).cleanup, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const CompletedRun: Story = { + play: async ({ canvasElement }) => { + await within(canvasElement).findByText("Reviewed the release checklist"); + }, +}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Empty: Story = { parameters: { fixtureState: "empty" } }; +export const Failed: Story = { + parameters: { fixtureState: "failure" }, + play: async ({ canvasElement }) => { + await within(canvasElement).findByText( + "The request failed. Please try again.", + ); + }, +}; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, + play: async ({ canvasElement }) => { + await within(canvasElement).findByText( + "You do not have permission to perform this action.", + ); + }, +}; +export const FilterNoMatches: Story = { + parameters: { initialEntries: ["/PAP/audit?runStatus=failed"] }, + play: async ({ canvasElement }) => { + await expect( + await within(canvasElement).findByRole("button", { + name: /Clear filters/, + }), + ).toBeEnabled(); + const c = within(canvasElement); + await expect( + await c.findByText("No runs match these filters."), + ).toBeVisible(); + await expect( + c.queryByText("Reviewed the release checklist"), + ).not.toBeInTheDocument(); + }, +}; diff --git a/ui/storybook/stories/agent-chat/conference-room.stories.tsx b/ui/storybook/stories/agent-chat/conference-room.stories.tsx new file mode 100644 index 00000000000..eb555e1ae38 --- /dev/null +++ b/ui/storybook/stories/agent-chat/conference-room.stories.tsx @@ -0,0 +1,120 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { BoardChat } from "@/pages/BoardChat"; +import { expect, userEvent, within } from "storybook/test"; +import { storybookIssues } from "../../fixtures/paperclipData"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +const meta = { + id: "feature-conference-room", + title: + "Tasks and conversations/Agent conversations and project handoff/Conference Room", + component: BoardChat, + parameters: { + layout: "fullscreen", + docs: { + description: { + component: + "Shipped Conference Room with isolated conversation receipts and SSE. This does not execute a provider or verify server access.", + }, + }, + }, + beforeEach: ({ parameters }) => { + let comments: unknown[] = []; + return prepareFixtureApi( + [ + { + path: "/api/companies/company-storybook/issues", + read: () => [ + { + ...storybookIssues[0], + id: "conference-gap", + title: "Board Operations", + }, + ], + }, + { path: "/api/issues/conference-gap/comments", read: () => comments }, + { path: "/api/issues/conference-gap/feedback-votes", read: () => [] }, + { + path: "/api/board/chat/stream", + method: "POST", + read: ({ body }) => { + comments = [ + { + id: "comment-human", + issueId: "conference-gap", + companyId: "company-storybook", + authorUserId: "user-storybook", + authorAgentId: null, + body: String(body.message), + createdAt: "2026-10-12T12:00:00Z", + updatedAt: "2026-10-12T12:00:00Z", + }, + { + id: "comment-agent", + issueId: "conference-gap", + companyId: "company-storybook", + authorUserId: null, + authorAgentId: "agent-codex", + body: "The release is ready for review.", + createdAt: "2026-10-12T12:00:01Z", + updatedAt: "2026-10-12T12:00:01Z", + }, + ]; + return new Response( + 'data: {"type":"chunk","text":"The release is ready for review."}\n\ndata: {"type":"done"}\n\n', + { headers: { "Content-Type": "text/event-stream" } }, + ); + }, + }, + ], + parameters.fixtureState as FixtureState, + ).cleanup; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Empty: Story = {}; +async function send(canvasElement: HTMLElement) { + const c = within(canvasElement); + await userEvent.type(await c.findByRole("textbox"), "Review the release", { + delay: 15, + }); + await userEvent.click(c.getByRole("button", { name: "Send message" })); +} +export const Responded: Story = { + play: async ({ canvasElement }) => { + await send(canvasElement); + await expect( + await within(canvasElement).findByText( + "The release is ready for review.", + ), + ).toBeVisible(); + }, +}; +export const Sending: Story = { + parameters: { fixtureState: "saving" }, + play: async ({ canvasElement }) => { + await send(canvasElement); + await within(canvasElement).findByText("Connecting..."); + }, +}; +export const SendFailed: Story = { + parameters: { fixtureState: "mutation-failure" }, + play: async ({ canvasElement }) => { + await send(canvasElement); + await within(canvasElement).findByText( + "The board assistant is unavailable right now. Please try again in a moment.", + ); + }, +}; +export const PermissionDenied: Story = { + parameters: { fixtureState: "mutation-permission" }, + play: async ({ canvasElement }) => { + await send(canvasElement); + await within(canvasElement).findByText( + "The board assistant is unavailable right now. Please try again in a moment.", + ); + }, +}; diff --git a/ui/storybook/stories/app-permissions/catalog-review.stories.tsx b/ui/storybook/stories/app-permissions/catalog-review.stories.tsx new file mode 100644 index 00000000000..4e14c58d30d --- /dev/null +++ b/ui/storybook/stories/app-permissions/catalog-review.stories.tsx @@ -0,0 +1,113 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { ToolCatalogEntry } from "@paperclipai/shared"; +import { ActionsSection } from "@/pages/apps/app-detail/PermissionsPanel"; +import { expect, userEvent, within } from "storybook/test"; +const entry: ToolCatalogEntry = { + id: "catalog-gap", + companyId: "company-storybook", + applicationId: null, + connectionId: "connection-gap", + entryKind: "tool", + toolName: "publish_release", + title: "Publish release", + description: "Publish the approved release notes.", + inputSchema: { type: "object" }, + outputSchema: null, + annotations: null, + riskLevel: "write", + isReadOnly: false, + isWrite: true, + isDestructive: false, + status: "quarantined", + addedAt: new Date("2026-10-12"), + version: null, + schemaHash: null, + firstSeenAt: new Date("2026-10-12"), + lastSeenAt: new Date("2026-10-12"), + reviewedAt: null, + reviewedByAgentId: null, + reviewedByUserId: null, + createdAt: new Date("2026-10-12"), + updatedAt: new Date("2026-10-12"), +}; +function CatalogReview({ + state = "ready", +}: { + state?: "ready" | "empty" | "refreshing" | "saving" | "readonly"; +}) { + const [quarantined, setQuarantined] = useState( + state === "empty" ? [] : [entry], + ); + const [enabled, setEnabled] = useState>(new Set()); + const [refreshing, setRefreshing] = useState(state === "refreshing"); + return ( + { + setEnabled(new Set(ids)); + setQuarantined([]); + }} + onRefreshActions={() => { + setRefreshing(false); + setQuarantined([entry]); + }} + onSetPermission={(ids, next) => + setEnabled(new Set(next === "allowed" ? ids : [])) + } + /> + ); +} +const meta = { + id: "feature-catalog-review", + title: + "Apps, channels, and extensions/App access and action permissions/Review newly discovered actions", + component: CatalogReview, + parameters: { + layout: "padded", + docs: { + description: { + component: + "Shipped action section and quarantine reviewer with local catalog props. Discovery failures belong to the owning AppDetail API boundary; permissions and review persistence need production checks.", + }, + }, + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Quarantined: Story = {}; +export const Empty: Story = { args: { state: "empty" } }; +export const Refreshing: Story = { args: { state: "refreshing" } }; +export const Saving: Story = { args: { state: "saving" } }; +export const ReadOnly: Story = { args: { state: "readonly" } }; +export const ReviewAndAllow: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click( + c.getByRole("switch", { name: "Publish release allowed" }), + ); + await userEvent.click(c.getByRole("button", { name: "Save choices" })); + await expect(c.queryByText("Review 1 new action")).not.toBeInTheDocument(); + await c.findByText("Publish release"); + }, +}; +export const LeaveBlocked: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click(c.getByRole("button", { name: "Save choices" })); + await expect(c.queryByText("Review 1 new action")).not.toBeInTheDocument(); + }, +}; diff --git a/ui/storybook/stories/budgets-costs/budget-resolution.stories.tsx b/ui/storybook/stories/budgets-costs/budget-resolution.stories.tsx new file mode 100644 index 00000000000..b7b272acbf9 --- /dev/null +++ b/ui/storybook/stories/budgets-costs/budget-resolution.stories.tsx @@ -0,0 +1,153 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { BudgetIncident } from "@paperclipai/shared"; +import { expect, userEvent, within, waitFor } from "storybook/test"; +import type { ComponentProps } from "react"; +import { Costs } from "@/pages/Costs"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +const incident: BudgetIncident = { + id: "budget-gap", + companyId: "company-storybook", + policyId: "policy-gap", + scopeType: "company", + scopeId: "company-storybook", + scopeName: "Acme Research", + metric: "billed_cents", + windowKind: "calendar_month_utc", + windowStart: new Date("2026-10-01"), + windowEnd: new Date("2026-11-01"), + thresholdType: "hard", + amountLimit: 10000, + amountObserved: 12000, + status: "open", + approvalId: null, + approvalStatus: null, + resolvedAt: null, + createdAt: new Date("2026-10-12"), + updatedAt: new Date("2026-10-12"), +}; + +let currentIncident: BudgetIncident; +function BudgetHost(props: NonNullable>) { + return ; +} +const meta = { + id: "feature-budget-resolution", + title: "Oversight and operation/Costs and budgets/Resolve a budget incident", + component: BudgetHost, + args: { embedded: true, initialTab: "budgets", lockTab: true }, + parameters: { layout: "padded" }, + beforeEach: ({ parameters }) => { + currentIncident = { ...incident }; + return prepareFixtureApi( + [ + { + path: "/api/companies/company-storybook/budgets/overview", + read: () => ({ + companyId: "company-storybook", + policies: [], + activeIncidents: + parameters.fixtureState === "empty" || + currentIncident.status !== "open" + ? [] + : [currentIncident], + pausedAgentCount: parameters.fixtureState==="empty"?0:currentIncident.status === "open" ? 1 : 0, + pausedProjectCount: 0, + pendingApprovalCount: 0, + }), + }, + { + path: "/api/companies/company-storybook/budget-incidents/budget-gap/resolve", + method: "POST", + read: ({ body }) => { + currentIncident = { + ...currentIncident, + status: body.action === "keep_paused" ? "dismissed" : "resolved", + resolvedAt: new Date(), + amountLimit: Number(body.amount ?? currentIncident.amountLimit), + }; + return currentIncident; + }, + }, + ], + parameters.fixtureState as FixtureState, + ).cleanup; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const DecisionRequired: Story = { + play: async ({ canvasElement }) => { + await within(canvasElement).findByRole("button", { + name: "Raise budget & resume", + }); + }, +}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Empty: Story = { parameters: { fixtureState: "empty" } }; +export const LoadFailed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; +export const InvalidAmount: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await c.findByRole("button", { name: "Raise budget & resume" }); + const input = c.getByPlaceholderText("0.00"); + await userEvent.clear(input); + await userEvent.type(input, "100", { delay: 15 }); + await expect( + c.getByRole("button", { name: "Raise budget & resume" }), + ).toBeDisabled(); + }, +}; +export const RaiseAndResume: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click( + await c.findByRole("button", { name: "Raise budget & resume" }), + ); + await waitFor(() => expect(currentIncident.status).toBe("resolved")); + await waitFor(() => + expect( + c.queryByRole("button", { name: "Raise budget & resume" }), + ).not.toBeInTheDocument(), + ); + }, +}; +export const KeepPaused: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click( + await c.findByRole("button", { name: "Keep paused" }), + ); + await waitFor(() => expect(currentIncident.status).toBe("dismissed")); + }, +}; +export const Saving: Story = { + parameters: { fixtureState: "saving" }, + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click( + await c.findByRole("button", { name: "Raise budget & resume" }), + ); + await expect( + await c.findByRole("button", { name: "Applying..." }), + ).toBeDisabled(); + }, +}; +export const SaveFailed: Story = { + parameters: { fixtureState: "mutation-failure" }, + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click( + await c.findByRole("button", { name: "Raise budget & resume" }), + ); + await c.findByRole("alert"); + await expect( + c.getByRole("button", { name: "Raise budget & resume" }), + ).toBeEnabled(); + }, +}; diff --git a/ui/storybook/stories/cases/case-browse.stories.tsx b/ui/storybook/stories/cases/case-browse.stories.tsx new file mode 100644 index 00000000000..d455deec822 --- /dev/null +++ b/ui/storybook/stories/cases/case-browse.stories.tsx @@ -0,0 +1,37 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Cases } from "@/pages/Cases"; +import { within } from "storybook/test"; +import { installCaseFixtures } from "../../support/capability-gaps/cases"; +import type { FixtureState } from "../../support/capability-gaps/api"; +const meta = { + id: "feature-case-browse", + title: "Automation and structured work/Cases/Browse and hierarchy", + component: Cases, + parameters: { layout: "padded" }, + beforeEach: ({ parameters }) => { + const key = "paperclip:cases:company-storybook:view"; + const previous = localStorage.getItem(key); + localStorage.removeItem(key); + const cleanup = installCaseFixtures( + parameters.fixtureState as FixtureState, + ); + return () => { + cleanup(); + if (previous) localStorage.setItem(key, previous); + else localStorage.removeItem(key); + }; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const ReviewCases: Story = { + play: async ({ canvasElement }) => { + await within(canvasElement).findAllByText("Release readiness"); + }, +}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Empty: Story = { parameters: { fixtureState: "empty" } }; +export const Failed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; diff --git a/ui/storybook/stories/cases/case-detail.stories.tsx b/ui/storybook/stories/cases/case-detail.stories.tsx new file mode 100644 index 00000000000..d789ca7a1c2 --- /dev/null +++ b/ui/storybook/stories/cases/case-detail.stories.tsx @@ -0,0 +1,87 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Routes, Route } from "@/lib/router"; +import { CaseDetail } from "@/pages/CaseDetail"; +import { CaseFieldsPanel } from "@/components/CaseFieldsPanel"; +import { IssueCasesPanel } from "@/components/IssueCasesPanel"; +import { instanceExperimentalSettingsSchema } from "@paperclipai/shared"; +import { expect, userEvent, within } from "storybook/test"; +import { + caseItem, + installCaseFixtures, +} from "../../support/capability-gaps/cases"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +function CaseDetailHost() { + return ( + + } /> + + ); +} +const meta = { + id: "feature-case-detail", + title: + "Automation and structured work/Cases/Structure, history and task links", + component: CaseDetailHost, + parameters: { layout: "padded", initialEntries: ["/PAP/cases/PAP-C42"] }, + beforeEach: ({ parameters }) => { + const cleanup = installCaseFixtures( + parameters.fixtureState as FixtureState, + ); + const flags = prepareFixtureApi([ + { + path: "/api/instance/settings/experimental", + read: () => ({ + ...instanceExperimentalSettingsSchema.parse({}), + enableCases: true, + }), + }, + ]); + return () => { + flags.cleanup(); + cleanup(); + }; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Detail: Story = { + play: async ({ canvasElement }) => { + await within(canvasElement).findByRole("heading", { + name: "Release readiness", + }); + }, +}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Failed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; +export const ActivityHistory: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click(await c.findByRole("tab", { name: /Activity/ })); + await c.findByText("Codex Coder"); + }, +}; +export const ArbitraryFields: Story = { + render: () => , +}; +export const EmptyFields: Story = { + render: () => , + play: async ({ canvasElement }) => { + await within(canvasElement).findByText("No fields set"); + }, +}; +export const LinkedFromTask: Story = { + render: () => , + play: async ({ canvasElement }) => { + await expect( + await within(canvasElement).findByRole("link", { + name: /Release readiness/, + }), + ).toHaveAttribute("href", "/PAP/cases/PAP-C42"); + }, +}; diff --git a/ui/storybook/stories/chat-channels/channel-attachments.stories.tsx b/ui/storybook/stories/chat-channels/channel-attachments.stories.tsx new file mode 100644 index 00000000000..3452549d93e --- /dev/null +++ b/ui/storybook/stories/chat-channels/channel-attachments.stories.tsx @@ -0,0 +1,87 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { IssueAttachment } from "@paperclipai/shared"; +import { TaskChatBubble } from "@/components/task-chat/TaskChatBubble"; +import { expect, within, waitFor } from "storybook/test"; +const attachment: IssueAttachment = { + id: "attachment-gap", + companyId: "company-storybook", + issueId: "task-gap", + issueCommentId: "message-gap", + assetId: "asset-gap", + contentPath: "/api/assets/asset-gap/content", + provider: "paperclip", + objectKey: "fixture/report.txt", + contentType: "text/plain", + byteSize: 42, + sha256: "fixture-sha256", + originalFilename: "release-report.txt", + createdByAgentId: null, + createdByUserId: "user-storybook", + createdAt: new Date("2026-10-12"), + updatedAt: new Date("2026-10-12"), +}; +const meta = { + id: "feature-channel-attachments", + title: + "Apps, channels, and extensions/External chat and email/Task-linked files", + component: TaskChatBubble, + args: { + item: { + id: "message-gap", + kind: "message", + author: "human", + text: "Here is the release report.", + sourceChannel: "imessage-photon", + }, + attachments: [attachment], + }, + parameters: { + layout: "padded", + docs: { + description: { + component: + "Production task bubble with the saved attachment receipt and channel attribution. Provider upload, delivery, download authority and replay require production tests.", + }, + }, + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const ReceivedFile: Story = { + play: async ({ canvasElement }) => { + await within(canvasElement).findByText("release-report.txt"); + }, +}; +export const NoFiles: Story = { args: { attachments: [] } }; +export const MultipleFiles: Story = { + args: { + attachments: [ + attachment, + { + ...attachment, + id: "attachment-two", + assetId: "asset-two", + contentPath: "/api/assets/asset-two/content", + originalFilename: "checks.json", + contentType: "application/json", + }, + ], + }, +}; +export const MissingFilename: Story = { + args: { attachments: [{ ...attachment, originalFilename: null }] }, +}; +export const OutboundFile: Story = { + args: { + item: { + id: "message-gap", + kind: "message", + author: "agent", + text: "I attached the reviewed release report.", + }, + }, + play: async ({ canvasElement }) => { + const title = await within(canvasElement).findByText("release-report.txt"); + await waitFor(() => expect(title).toBeVisible()); + }, +}; diff --git a/ui/storybook/stories/companies/company-import.stories.tsx b/ui/storybook/stories/companies/company-import.stories.tsx new file mode 100644 index 00000000000..2a71d23d3c5 --- /dev/null +++ b/ui/storybook/stories/companies/company-import.stories.tsx @@ -0,0 +1,126 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { CompanyImport } from "@/pages/CompanyImport"; +import { expect, userEvent, within } from "storybook/test"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +const files = { + ".paperclip.yaml": "schema: paperclip/v1\n", + "agents/reviewer/AGENTS.md": "# Reviewer\n\nReview release evidence.", +}; +const preview = { + include: { company: true, agents: true, projects: true, issues: true }, + targetCompanyId: null, + targetCompanyName: null, + collisionStrategy: "rename", + selectedAgentSlugs: ["reviewer"], + plan: { + companyAction: "create", + agentPlans: [ + { + slug: "reviewer", + action: "create", + plannedName: "Reviewer", + existingAgentId: null, + reason: null, + }, + ], + projectPlans: [], + issuePlans: [], + }, + manifest: { + agents: [ + { + slug: "reviewer", + name: "Reviewer", + path: "agents/reviewer/AGENTS.md", + adapterType: "claude_local", + }, + ], + projects: [], + issues: [], + skills: [], + company: null, + }, + files, + envInputs: [], + warnings: [], + errors: [], +}; +const meta = { + id: "feature-company-import", + title: "Getting started and access/Companies and portability/Import preview", + component: CompanyImport, + parameters: { layout: "padded" }, + beforeEach: ({ parameters }) => + prepareFixtureApi( + [ + { + path: "/api/companies/import/preview", + method: "POST", + read: () => ({ + ...preview, + warnings: parameters.warning + ? ["Provider credentials are not included in this package."] + : [], + }), + }, + ], + parameters.fixtureState as FixtureState, + ).cleanup, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const ChooseSource: Story = {}; +async function previewPackage(canvasElement: HTMLElement) { + const c = within(canvasElement); + await userEvent.type( + await c.findByPlaceholderText( + "https://github.com/owner/repo/tree/main/company", + ), + "https://github.com/example/release-team", + { delay: 15 }, + ); + await userEvent.click(c.getByRole("button", { name: "Preview import" })); +} +export const PreviewReady: Story = { + play: async ({ canvasElement }) => { + await previewPackage(canvasElement); + await within(canvasElement).findByText("Reviewer"); + }, +}; +export const PreviewWithWarning: Story = { + parameters: { warning: true }, + play: async ({ canvasElement }) => { + await previewPackage(canvasElement); + await within(canvasElement).findByText( + "Provider credentials are not included in this package.", + ); + }, +}; +export const PreviewLoading: Story = { + parameters: { fixtureState: "saving" }, + play: async ({ canvasElement }) => { + await previewPackage(canvasElement); + await expect( + await within(canvasElement).findByRole("button", { + name: "Previewing...", + }), + ).toBeDisabled(); + }, +}; +export const PreviewFailed: Story = { + parameters: { fixtureState: "mutation-failure" }, + play: async ({ canvasElement }) => { + await previewPackage(canvasElement); + await within(canvasElement).findByText(/The request failed/); + }, +}; +export const PermissionDenied: Story = { + parameters: { fixtureState: "mutation-permission" }, + play: async ({ canvasElement }) => { + await previewPackage(canvasElement); + await within(canvasElement).findByText(/You do not have permission/); + }, +}; diff --git a/ui/storybook/stories/companies/company-selection.stories.tsx b/ui/storybook/stories/companies/company-selection.stories.tsx new file mode 100644 index 00000000000..539bd8fa154 --- /dev/null +++ b/ui/storybook/stories/companies/company-selection.stories.tsx @@ -0,0 +1,88 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Companies } from "@/pages/Companies"; +import { expect, userEvent, within, waitFor } from "storybook/test"; +import { storybookCompanies } from "../../fixtures/paperclipData"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +const meta = { + id: "feature-company-selection", + title: + "Getting started and access/Companies and portability/Selection and archived organizations", + component: Companies, + parameters: { layout: "padded" }, + beforeEach: ({ parameters }) => { + let companies = + parameters.fixtureState === "empty" + ? [] + : [ + ...storybookCompanies.map((c) => ({ ...c })), + { + ...storybookCompanies[0], + id: "archived-gap", + name: "Archived Research", + issuePrefix: "ARC", + status: "archived", + }, + ]; + return prepareFixtureApi( + [ + { path: "/api/companies", read: () => companies }, + { path: "/api/companies/stats", read: () => ({}) }, + { + path: "/api/companies/archived-gap", + method: "PATCH", + read: ({ body }) => { + companies = companies.map((c) => + c.id === "archived-gap" ? { ...c, ...body } : c, + ); + return companies.find((c) => c.id === "archived-gap"); + }, + }, + ], + parameters.fixtureState as FixtureState, + ).cleanup; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Organizations: Story = {}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Empty: Story = { parameters: { fixtureState: "empty" } }; +export const Failed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; +export const SelectOrganization: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + const card = await c.findByRole("button", { name: /Archived Research/ }); + await userEvent.click(card); + await waitFor(() => + expect(localStorage.getItem("paperclip.selectedCompanyId")).toBe( + "archived-gap", + ), + ); + }, +}; +export const Unarchive: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + const card = (await c.findByText("Archived Research")).closest( + '[role="button"]', + ) as HTMLElement; + await userEvent.click( + card.querySelector('button[aria-haspopup="menu"]')!, + ); + await userEvent.click( + await within(document.body).findByRole("menuitem", { name: "Unarchive" }), + ); + await waitFor(() => { + const restored = within(canvasElement) + .getByText("Archived Research") + .closest('[role="button"]') as HTMLElement; + expect(within(restored).getByText("active")).toBeVisible(); + }); + }, +}; diff --git a/ui/storybook/stories/documents-artifacts/document-revisions.stories.tsx b/ui/storybook/stories/documents-artifacts/document-revisions.stories.tsx new file mode 100644 index 00000000000..a0823331855 --- /dev/null +++ b/ui/storybook/stories/documents-artifacts/document-revisions.stories.tsx @@ -0,0 +1,169 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { IssueDocumentsSection } from "@/components/IssueDocumentsSection"; +import { expect, userEvent, within } from "storybook/test"; +import { + storybookIssues, + storybookIssueDocuments, +} from "../../fixtures/paperclipData"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +const meta = { + id: "feature-document-revisions", + title: + "Tasks and conversations/Documents, attachments, and work products/Document revisions", + component: IssueDocumentsSection, + args: { issue: storybookIssues[0], canDeleteDocuments: true }, + parameters: { + layout: "padded", + docs: { + description: { + component: + "Production document section. Loading and read errors currently yield the section's empty rendering; these examples expose that limitation. Revision authority and persisted concurrency require API tests.", + }, + }, + }, + beforeEach: ({ parameters }) => { + let doc = { + ...storybookIssueDocuments[0], + body: "# Release plan\n\nRun the release checks.", + latestRevisionNumber: 2, + latestRevisionId: "revision-latest", + }; + const revisions = [ + { + id: "revision-latest", + companyId: doc.companyId, + documentId: doc.id, + issueId: doc.issueId, + key: doc.key, + revisionNumber: 2, + title: doc.title, + format: "markdown", + body: doc.body, + changeSummary: "Add checks", + createdByAgentId: null, + createdByUserId: "user-storybook", + createdAt: "2026-10-12T12:00:00Z", + }, + { + id: "revision-first", + companyId: doc.companyId, + documentId: doc.id, + issueId: doc.issueId, + key: doc.key, + revisionNumber: 1, + title: doc.title, + format: "markdown", + body: "# Original release plan", + changeSummary: "Initial draft", + createdByAgentId: null, + createdByUserId: "user-storybook", + createdAt: "2026-10-11T12:00:00Z", + }, + ]; + return prepareFixtureApi( + [ + { + path: `/api/issues/${doc.issueId}/documents`, + read: () => (parameters.fixtureState === "empty" ? [] : [doc]), + }, + { + path: `/api/issues/${doc.issueId}/documents/${doc.key}/revisions`, + read: () => revisions, + }, + { + path: `/api/issues/${doc.issueId}/documents/${doc.key}`, + read: ({ method, body }) => { + if (method === "PUT") + doc = { + ...doc, + body: String(body.body), + latestRevisionNumber: doc.latestRevisionNumber + 1, + }; + return doc; + }, + }, + { + path: `/api/issues/${doc.issueId}/documents/${doc.key}/revisions/revision-first/restore`, + method: "POST", + read: () => { + doc = { + ...doc, + body: revisions[1].body, + latestRevisionId: "revision-restored", + latestRevisionNumber: 3, + }; + revisions.unshift({ + ...revisions[0], + id: "revision-restored", + revisionNumber: 3, + body: doc.body, + }); + return doc; + }, + }, + ], + parameters.fixtureState as FixtureState, + ).cleanup; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Current: Story = { + play: async ({ canvasElement }) => { + await within(canvasElement).findByText("Run the release checks."); + }, +}; +export const Empty: Story = { parameters: { fixtureState: "empty" } }; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const ReadFailed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; +export const Edit: Story = { args: { forceEditDocumentKey: "plan" } }; + +async function previousRevision(canvasElement: HTMLElement) { + await userEvent.click( + await within(canvasElement).findByRole("button", { name: /rev 2/ }), + ); + await userEvent.click( + await within(document.body).findByRole("menuitemradio", { name: /rev 1/ }), + ); + await within(canvasElement).findByText("Original release plan"); +} +export const HistoricalPreview: Story = { + play: async ({ canvasElement }) => { + await previousRevision(canvasElement); + await within(canvasElement).findByRole("button", { + name: "Restore this revision", + }); + }, +}; +export const Restored: Story = { + play: async ({ canvasElement }) => { + await previousRevision(canvasElement); + await userEvent.click( + within(canvasElement).getByRole("button", { + name: "Restore this revision", + }), + ); + await within(canvasElement).findByRole("button", { name: /rev 3/ }); + await within(canvasElement).findByText("Original release plan"); + }, +}; +export const RestoreFailed: Story = { + parameters: { fixtureState: "mutation-failure" }, + play: async ({ canvasElement }) => { + await previousRevision(canvasElement); + await userEvent.click( + within(canvasElement).getByRole("button", { + name: "Restore this revision", + }), + ); + await within(canvasElement).findByText( + "The request failed. Please try again.", + ); + }, +}; diff --git a/ui/storybook/stories/execution-environments/custom-images.stories.tsx b/ui/storybook/stories/execution-environments/custom-images.stories.tsx new file mode 100644 index 00000000000..83527f3054b --- /dev/null +++ b/ui/storybook/stories/execution-environments/custom-images.stories.tsx @@ -0,0 +1,106 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Routes, Route } from "@/lib/router"; +import { CompanyEnvironments } from "@/pages/CompanyEnvironments"; +import { ToastViewport } from "@/components/ToastViewport"; +import { expect, userEvent, within } from "storybook/test"; +import { installEnvironmentFixture } from "../../support/capability-gaps/environments"; +import type { FixtureState } from "../../support/capability-gaps/api"; +function Environments() { + return ( + <> + + } + /> + } + /> + + + + ); +} +const meta = { + id: "feature-environment-images", + title: "Projects and execution/Execution environments/Custom images", + component: Environments, + parameters: { + layout: "padded", + initialEntries: ["/PAP/company/settings/instance/environments"], + docs: { + description: { + component: + "Production environment admin page. API replies are fictional; no probe connects to a provider. List loading/errors currently fall back to empty saved environments rather than dedicated feedback.", + }, + }, + }, + beforeEach: ({ parameters }) => + installEnvironmentFixture( + parameters.fixtureState as FixtureState, + parameters.image, + parameters.unsupported, + ).cleanup, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const NoImage: Story = { + parameters: { + initialEntries: [ + "/PAP/company/settings/instance/environments/environment-gap/edit", + ], + }, +}; +export const CapturedImage: Story = { + play: async ({ canvasElement }) => { + await within(canvasElement).findByText("Active template"); + }, + parameters: { + image: "captured", + initialEntries: [ + "/PAP/company/settings/instance/environments/environment-gap/edit", + ], + }, +}; +export const ImageReadFailed: Story = { + parameters: { + image: "failed", + initialEntries: [ + "/PAP/company/settings/instance/environments/environment-gap/edit", + ], + }, +}; +export const UnsupportedProvider: Story = { + parameters: { + unsupported: true, + initialEntries: [ + "/PAP/company/settings/instance/environments/environment-gap/edit", + ], + }, +}; + +export const ImageLoading: Story = { + parameters: { + image: "loading", + initialEntries: [ + "/PAP/company/settings/instance/environments/environment-gap/edit", + ], + }, + play: async ({ canvasElement }) => { + await within(canvasElement).findByText("Loading template setup..."); + }, +}; +export const ImagePermissionDenied: Story = { + parameters: { + image: "denied", + initialEntries: [ + "/PAP/company/settings/instance/environments/environment-gap/edit", + ], + }, + play: async ({ canvasElement }) => { + await within(canvasElement).findByText( + "You do not have permission to inspect this image.", + ); + }, +}; diff --git a/ui/storybook/stories/execution-environments/environment-states.stories.tsx b/ui/storybook/stories/execution-environments/environment-states.stories.tsx new file mode 100644 index 00000000000..78511fec7cc --- /dev/null +++ b/ui/storybook/stories/execution-environments/environment-states.stories.tsx @@ -0,0 +1,103 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Routes, Route } from "@/lib/router"; +import { CompanyEnvironments } from "@/pages/CompanyEnvironments"; +import { ToastViewport } from "@/components/ToastViewport"; +import { expect, userEvent, within } from "storybook/test"; +import { installEnvironmentFixture } from "../../support/capability-gaps/environments"; +import type { FixtureState } from "../../support/capability-gaps/api"; +function Environments() { + return ( + <> + + } + /> + } + /> + + + + ); +} +const meta = { + id: "feature-execution-environments", + title: + "Projects and execution/Execution environments/Configuration, capabilities, and probes", + component: Environments, + parameters: { + layout: "padded", + initialEntries: ["/PAP/company/settings/instance/environments"], + docs: { + description: { + component: + "Production environment admin page. API replies are fictional; no probe connects to a provider. List loading/errors currently fall back to empty saved environments rather than dedicated feedback.", + }, + }, + }, + beforeEach: ({ parameters }) => + installEnvironmentFixture( + parameters.fixtureState as FixtureState, + parameters.image, + parameters.unsupported, + ).cleanup, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Configured: Story = { + play: async ({ canvasElement }) => { + await within(canvasElement).findByText("Release sandbox"); + }, +}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Empty: Story = { parameters: { fixtureState: "empty" } }; +export const Failed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; +export const UnsupportedProvider: Story = { parameters: { unsupported: true } }; +export const ProbeSucceeded: Story = { + play: async ({ canvasElement }) => { + await userEvent.click( + await within(canvasElement).findByRole("button", { + name: "Test provider", + }), + ); + await within(document.body).findAllByText("Provider connection succeeded."); + }, +}; +export const Probing: Story = { + parameters: { fixtureState: "saving" }, + play: async ({ canvasElement }) => { + await userEvent.click( + await within(canvasElement).findByRole("button", { + name: "Test provider", + }), + ); + await expect( + await within(canvasElement).findByRole("button", { name: "Testing..." }), + ).toBeDisabled(); + }, +}; +export const ProbeFailed: Story = { + parameters: { fixtureState: "mutation-failure" }, + play: async ({ canvasElement }) => { + await userEvent.click( + await within(canvasElement).findByRole("button", { + name: "Test provider", + }), + ); + await within(document.body).findAllByText( + "The request failed. Please try again.", + ); + }, +}; +export const EditConfiguration: Story = { + parameters: { + initialEntries: [ + "/PAP/company/settings/instance/environments/environment-gap/edit", + ], + }, +}; diff --git a/ui/storybook/stories/gateways-profiles/gateway-tokens.stories.tsx b/ui/storybook/stories/gateways-profiles/gateway-tokens.stories.tsx new file mode 100644 index 00000000000..f111d99654e --- /dev/null +++ b/ui/storybook/stories/gateways-profiles/gateway-tokens.stories.tsx @@ -0,0 +1,220 @@ +import { useQuery } from "@tanstack/react-query"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { + ToolMcpGatewayToken, + ToolMcpGatewayWithTokens, +} from "@paperclipai/shared"; +import { + toolMcpGatewayAuthConfigSchema, + toolMcpGatewayHeaderPolicySchema, + toolMcpGatewayMetadataPolicySchema, + toolMcpGatewayOnDemandToolsConfigSchema, +} from "@paperclipai/shared"; +import { expect, userEvent, within } from "storybook/test"; +import { toolsApi } from "@/api/tools"; +import { TokensPanel } from "@/pages/apps/gateways/panels/TokensPanel"; +import { gatewaysQueryKey } from "@/pages/apps/gateways/NewGatewayDialog"; +import { ToastViewport } from "@/components/ToastViewport"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +const token: ToolMcpGatewayToken = { + id: "token-gap", + companyId: "company-storybook", + gatewayId: "gateway-gap", + name: "Preview client", + tokenPrefix: "pcgw_fixture", + subjectType: "gateway_client", + subjectId: null, + clientLabel: "Preview client", + ownerNote: "Local preview", + allowedActions: ["tools/list", "tools/call"], + expiresAt: "2099-12-01T00:00:00Z", + expiryOverrideReason: null, + expiryOverrideByUserId: null, + expiryOverrideByAgentId: null, + expiryOverrideAt: null, + lastUsedAt: null, + revokedAt: null, + createdByAgentId: null, + createdByUserId: "user-storybook", + createdAt: "2026-10-12T12:00:00Z", + updatedAt: "2026-10-12T12:00:00Z", +}; +const gateway: ToolMcpGatewayWithTokens = { + id: "gateway-gap", + companyId: "company-storybook", + gatewayPublicId: "preview-gateway", + name: "Preview gateway", + displaySlug: "preview", + slug: "preview", + description: null, + status: "active", + profileId: "profile-gap", + defaultProfileMode: "gateway_only", + contextScopeType: "none", + contextScopeId: null, + agentId: null, + projectId: null, + issueId: null, + approvalIssueId: null, + endpointPath: "/g/preview/mcp", + authConfig: toolMcpGatewayAuthConfigSchema.parse({}), + headerPolicy: toolMcpGatewayHeaderPolicySchema.parse({}), + metadataPolicy: toolMcpGatewayMetadataPolicySchema.parse({}), + onDemandToolsConfig: toolMcpGatewayOnDemandToolsConfigSchema.parse({}), + metadata: null, + createdByAgentId: null, + createdByUserId: "user-storybook", + archivedAt: null, + createdAt: new Date("2026-10-12"), + updatedAt: new Date("2026-10-12"), + tokens: [token], + clientSnippets: [], +}; +function Tokens() { + const query = useQuery({ + queryKey: gatewaysQueryKey("company-storybook"), + queryFn: () => toolsApi.listGateways("company-storybook"), + }); + if (!query.data) return null; + return ( + <> + + + + ); +} +const meta = { + id: "feature-gateway-tokens", + title: + "Apps, channels, and extensions/Tool gateways and access profiles/Named gateway tokens", + component: Tokens, + parameters: { layout: "padded" }, + beforeEach: ({ parameters }) => { + let current = { + ...gateway, + tokens: parameters.empty + ? [] + : [ + { + ...token, + expiresAt: parameters.expired + ? "2020-01-01T00:00:00Z" + : token.expiresAt, + }, + ], + }; + return prepareFixtureApi( + [ + { + path: "/api/companies/company-storybook/tools/gateways", + read: () => ({ gateways: [current] }), + }, + { + path: "/api/tool-gateway/gateways/gateway-gap/tokens", + method: "POST", + read: ({ body }) => { + const created = { + ...token, + id: "issued-token", + name: String(body.name), + clientLabel: String(body.name), + ownerNote: String(body.ownerNote), + expiresAt: String(body.expiresAt), + }; + current = { ...current, tokens: [created, ...current.tokens] }; + return { ...created, token: "pcgw_storybook_fixture_only" }; + }, + }, + { + path: "/api/tool-gateway/gateway-tokens/token-gap/revoke", + method: "POST", + read: () => { + const revoked = { ...token, revokedAt: "2026-10-13T12:00:00Z" }; + current = { + ...current, + tokens: current.tokens.map((row) => + row.id === token.id ? revoked : row, + ), + }; + return revoked; + }, + }, + ], + parameters.fixtureState as FixtureState, + ).cleanup; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const ActiveToken: Story = {}; +export const NoTokens: Story = { parameters: { empty: true } }; +export const ExpiredToken: Story = { parameters: { expired: true } }; +async function issue(canvasElement: HTMLElement) { + const c = within(canvasElement); + await userEvent.click(await c.findByRole("button", { name: "Issue token" })); + await userEvent.clear(c.getByRole("textbox", { name: /Token name/ })); + await userEvent.type( + c.getByRole("textbox", { name: /Token name/ }), + "Review client", + { delay: 15 }, + ); + await userEvent.click( + within(canvasElement.querySelector("form")!).getByRole("button", { + name: "Issue token", + }), + ); +} +export const IssuedToken: Story = { + play: async ({ canvasElement }) => { + await issue(canvasElement); + await within(document.body).findByText("Token issued"); + await within(canvasElement).findByText("pcgw_storybook_fixture_only"); + }, +}; +export const Issuing: Story = { + parameters: { fixtureState: "saving" }, + play: async ({ canvasElement }) => { + await issue(canvasElement); + await expect( + await within(canvasElement).findByRole("button", { name: "Issuing…" }), + ).toBeDisabled(); + }, +}; +export const IssueFailed: Story = { + parameters: { fixtureState: "mutation-failure" }, + play: async ({ canvasElement }) => { + await issue(canvasElement); + await within(document.body).findByText("Token was not issued"); + }, +}; +export const PermissionDenied: Story = { + parameters: { fixtureState: "mutation-permission" }, + play: async ({ canvasElement }) => { + await issue(canvasElement); + await within(document.body).findByText( + "You do not have permission to perform this action.", + ); + }, +}; +export const RevokeToken: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click( + await c.findByRole("button", { name: /Token history/ }), + ); + await userEvent.click(await c.findByRole("button", { name: "Revoke" })); + await userEvent.type( + c.getByPlaceholderText("Preview client"), + "Preview client", + { delay: 15 }, + ); + await userEvent.click(c.getByRole("button", { name: "Revoke token" })); + await within(document.body).findByText("Token revoked"); + }, +}; diff --git a/ui/storybook/stories/goals/goal-alignment.stories.tsx b/ui/storybook/stories/goals/goal-alignment.stories.tsx new file mode 100644 index 00000000000..a55d2d27b6a --- /dev/null +++ b/ui/storybook/stories/goals/goal-alignment.stories.tsx @@ -0,0 +1,94 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Routes, Route } from "@/lib/router"; +import { GoalDetail } from "@/pages/GoalDetail"; +import { expect, userEvent, within } from "storybook/test"; +import { + storybookGoals, + storybookProjects, +} from "../../fixtures/paperclipData"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +function Goal() { + return ( + + } /> + + ); +} +const goal = { + ...storybookGoals[0], + id: "goal-gap", + title: "Qualify the release", +}; +const meta = { + id: "feature-goal-alignment", + title: + "Projects and execution/Goals and work alignment/Aligned work and navigation", + component: Goal, + parameters: { layout: "padded", initialEntries: ["/PAP/goals/goal-gap"] }, + beforeEach: ({ parameters }) => + prepareFixtureApi( + [ + { path: "/api/goals/goal-gap", read: () => goal }, + { + path: "/api/companies/company-storybook/goals", + read: () => + parameters.fixtureState === "empty" + ? [goal] + : [ + goal, + { + ...goal, + id: "child-goal-gap", + title: "Verify provider access", + parentId: goal.id, + }, + ], + }, + { + path: "/api/companies/company-storybook/projects", + read: () => + parameters.fixtureState === "empty" + ? [] + : [ + { + ...storybookProjects[0], + name: "Release workspace", + goalId: goal.id, + goalIds: [goal.id], + goals: [{ id: goal.id, title: goal.title }], + }, + ], + }, + ], + parameters.fixtureState as FixtureState, + ).cleanup, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const GoalAndChildren: Story = { + play: async ({ canvasElement }) => { + await expect( + await within(canvasElement).findByRole("link", { + name: /Verify provider access/, + }), + ).toHaveAttribute("href", "/PAP/goals/child-goal-gap"); + }, +}; +export const AlignedProject: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click(await c.findByRole("tab", { name: /Projects/ })); + await c.findByText("Release workspace"); + }, +}; +export const EmptyRelationships: Story = { + parameters: { fixtureState: "empty" }, +}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Failed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; diff --git a/ui/storybook/stories/inbox-search/inbox-views.stories.tsx b/ui/storybook/stories/inbox-search/inbox-views.stories.tsx new file mode 100644 index 00000000000..9ae2fca438c --- /dev/null +++ b/ui/storybook/stories/inbox-search/inbox-views.stories.tsx @@ -0,0 +1,65 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { ComponentProps } from "react"; +import { Inbox } from "@/pages/Inbox"; +import { instanceExperimentalSettingsSchema } from "@paperclipai/shared"; +import { storybookIssues } from "../../fixtures/paperclipData"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +function InboxHost(props: NonNullable>) { + return ; +} +const meta = { + id: "feature-inbox-views", + title: + "Tasks and conversations/Inbox, decisions, and search/Personal task views", + component: InboxHost, + args: { tab: "mine" }, + parameters: { layout: "padded" }, + beforeEach: ({ parameters }) => { + const api = prepareFixtureApi( + [ + { + path: "/api/companies/company-storybook/issues", + read: () => + parameters.fixtureState === "empty" ? [] : storybookIssues, + }, + { + path: /^\/api\/companies\/company-storybook\/(approvals|join-requests|heartbeat-runs)$/, + read: () => [], + }, + ], + parameters.fixtureState as FixtureState, + ); + const flags = prepareFixtureApi([ + { + path: "/api/instance/settings/experimental", + read: () => ({ + ...instanceExperimentalSettingsSchema.parse({}), + enableStreamlinedUi: true, + enableCombinedInboxTasks: true, + }), + }, + ]); + return () => { + flags.cleanup(); + api.cleanup(); + }; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Mine: Story = { args: { tab: "mine" } }; +export const Recent: Story = { args: { tab: "recent" } }; +export const Unread: Story = { args: { tab: "unread" } }; +export const Blocked: Story = { args: { tab: "blocked" } }; +export const Empty: Story = { + args: { tab: "mine" }, + parameters: { fixtureState: "empty" }, +}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Failed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; diff --git a/ui/storybook/stories/inbox-search/personal-task-toasts.stories.tsx b/ui/storybook/stories/inbox-search/personal-task-toasts.stories.tsx new file mode 100644 index 00000000000..8c96408330c --- /dev/null +++ b/ui/storybook/stories/inbox-search/personal-task-toasts.stories.tsx @@ -0,0 +1,113 @@ +import { useEffect, useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { LiveUpdatesProvider } from "@/context/LiveUpdatesProvider"; +import { ToastViewport } from "@/components/ToastViewport"; +import { Button } from "@/components/ui/button"; +import { expect, userEvent, within, waitFor } from "storybook/test"; +import { + installLiveEventFixture, + listenForFixtureSocket, + publishFixtureEvent, +} from "../../support/capability-gaps/live-events"; +function Publish({ eligible = true }: { eligible?: boolean }) { + const [ready, setReady] = useState(false); + useEffect(() => listenForFixtureSocket(() => setReady(true)), []); + return ( + <> + + + + ); +} +function TaskNotifications(props: { eligible?: boolean }) { + return ( + + + + ); +} +const meta = { + id: "feature-personal-task-toasts", + title: + "Tasks and conversations/Inbox, decisions, and search/Personal task notifications", + component: TaskNotifications, + parameters: { + layout: "padded", + initialEntries: ["/PAP/dashboard"], + docs: { + description: { + component: + "Production socket consumer and toast viewport. Server eligibility metadata is simulated; persisted participation and mentions require multiplayer tests.", + }, + }, + }, + beforeEach: installLiveEventFixture, +} satisfies Meta; +export default meta; +type Story = StoryObj; +async function publish(canvasElement: HTMLElement) { + const button = await within(canvasElement).findByRole("button", { + name: "Publish task update", + }); + await waitFor(() => expect(button).toBeEnabled()); + await userEvent.click(button); +} +export const EligibleTask: Story = { + play: async ({ canvasElement }) => { + await publish(canvasElement); + await within(document.body).findByText(/Release reviewer created/); + }, +}; +export const UnrelatedTaskStaysQuiet: Story = { + args: { eligible: false }, + play: async ({ canvasElement }) => { + await publish(canvasElement); + await expect( + within(document.body).queryByText(/Release reviewer created/), + ).not.toBeInTheDocument(); + }, +}; +export const VisibleTaskStaysQuiet: Story = { + parameters: { initialEntries: ["/PAP/issues/toast-task"] }, + play: async ({ canvasElement }) => { + await publish(canvasElement); + await expect( + within(document.body).queryByText(/Release reviewer created/), + ).not.toBeInTheDocument(); + }, +}; +export const DuplicateDelivery: Story = { + play: async ({ canvasElement }) => { + await publish(canvasElement); + await within(document.body).findByText(/Release reviewer created/); + await publish(canvasElement); + await expect( + within(document.body).getAllByText(/Release reviewer created/), + ).toHaveLength(1); + }, +}; diff --git a/ui/storybook/stories/instance-operations/backup-retention.stories.tsx b/ui/storybook/stories/instance-operations/backup-retention.stories.tsx new file mode 100644 index 00000000000..32962c98465 --- /dev/null +++ b/ui/storybook/stories/instance-operations/backup-retention.stories.tsx @@ -0,0 +1,93 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, userEvent, within, waitFor } from "storybook/test"; +import { instanceGeneralSettingsSchema } from "@paperclipai/shared"; +import { InstanceGeneralSettings } from "@/pages/InstanceGeneralSettings"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +let settings = instanceGeneralSettingsSchema.parse({}); +const meta = { + id: "feature-backup-retention", + title: "Oversight and operation/Instance operations/Backup retention", + component: InstanceGeneralSettings, + parameters: { + layout: "padded", + docs: { + description: { + component: + "Shipped general settings. Retention changes persist within this fixture; database backup and restoration require operator verification.", + }, + }, + }, + beforeEach: ({ parameters }) => { + settings = instanceGeneralSettingsSchema.parse({}); + const fixture = prepareFixtureApi( + [ + { + path: "/api/instance/settings/general", + read: ({ method, body }) => { + if (method === "PATCH") + settings = instanceGeneralSettingsSchema.parse({ + ...settings, + ...body, + }); + return settings; + }, + }, + ], + parameters.fixtureState as FixtureState, + ); + return fixture.cleanup; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const CurrentPolicy: Story = { + play: async ({ canvasElement }) => { + await within(canvasElement).findByRole("heading", { + name: "Backup retention", + }); + }, +}; +export const Loading: Story = { + parameters: { fixtureState: "loading" }, + play: async ({ canvasElement }) => { + await within(canvasElement).findByText("Loading general settings..."); + }, +}; +export const LoadFailed: Story = { + parameters: { fixtureState: "failure" }, + play: async ({ canvasElement }) => { + await within(canvasElement).findByText( + "The request failed. Please try again.", + ); + }, +}; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, + play: async ({ canvasElement }) => { + await within(canvasElement).findByText( + "You do not have permission to perform this action.", + ); + }, +}; +export const SaveRetention: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + const button = await c.findByRole("button", { name: "14 days" }); + await userEvent.click(button); + await waitFor(() => expect(settings.backupRetention.dailyDays).toBe(14)); + await waitFor(() => expect(button).toBeEnabled()); + await expect(button).toHaveClass("bg-accent"); + }, +}; +export const Saving: Story = { + parameters: { fixtureState: "saving" }, + play: async ({ canvasElement }) => { + const c = within(canvasElement); + const button = await c.findByRole("button", { name: "14 days" }); + await userEvent.click(button); + await expect(button).toBeDisabled(); + }, +}; diff --git a/ui/storybook/stories/navigation-preferences/sidebar-preferences.stories.tsx b/ui/storybook/stories/navigation-preferences/sidebar-preferences.stories.tsx new file mode 100644 index 00000000000..edaaf5da008 --- /dev/null +++ b/ui/storybook/stories/navigation-preferences/sidebar-preferences.stories.tsx @@ -0,0 +1,65 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { PluginLauncherProvider } from "@/plugins/launchers"; +import { Sidebar } from "@/components/Sidebar"; +import { expect, userEvent, within, waitFor } from "storybook/test"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +function SidebarPreferences() { + return ( + + + + ); +} +const meta = { + id: "feature-sidebar-preferences", + title: + "Oversight and operation/Navigation, profile, and announcements/Sidebar preferences", + component: SidebarPreferences, + parameters: { + layout: "padded", + docs: { + description: { + component: + "Production sidebar with isolated saved order responses. Section collapse is session-local; recents are browser-local. No server persistence is claimed.", + }, + }, + }, + beforeEach: ({ parameters }) => + prepareFixtureApi( + [ + { + path: "/api/sidebar-preferences/me", + read: () => ({ orderedIds: ["company-storybook"], updatedAt: null }), + }, + { + path: "/api/companies/company-storybook/sidebar-preferences/me", + read: () => ({ orderedIds: [], updatedAt: null }), + }, + ], + parameters.fixtureState as FixtureState, + ).cleanup, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const SavedOrder: Story = {}; +export const LoadingOrder: Story = { parameters: { fixtureState: "loading" } }; +export const OrderReadFailed: Story = { + parameters: { fixtureState: "failure" }, +}; +export const OrderPermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; +export const CollapseWork: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click(await c.findByRole("button", { name: /Work/ })); + await waitFor(() => + expect( + c.queryByRole("link", { name: "Dashboard" }), + ).not.toBeInTheDocument(), + ); + }, +}; diff --git a/ui/storybook/stories/onboarding/resume-draft.stories.tsx b/ui/storybook/stories/onboarding/resume-draft.stories.tsx new file mode 100644 index 00000000000..bdaec9128ca --- /dev/null +++ b/ui/storybook/stories/onboarding/resume-draft.stories.tsx @@ -0,0 +1,111 @@ +import { useEffect, useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, userEvent, within, waitFor } from "storybook/test"; +import { + OnboardingWizard, + ONBOARDING_STORAGE_KEY, +} from "@/components/OnboardingWizard"; +import { useDialog } from "@/context/DialogContext"; +import { useCompanyListQuery } from "@/api/companies-query"; +import { Button } from "@/components/ui/button"; +import { + seedOnboardingDraft, + clearOnboardingDraft, +} from "../../fixtures/onboardingDraft"; +function Resume() { + const companies = useCompanyListQuery(); + const { openOnboarding } = useDialog(); + const [generation, setGeneration] = useState(0); + useEffect(() => { + if (companies.isSuccess) + openOnboarding({ initialStep: 3, companyId: "company-storybook" }); + }, [companies.isSuccess, openOnboarding]); + return ( + <> + {companies.isSuccess ? : null} + + + ); +} +const meta = { + id: "feature-onboarding-resume", + title: + "Getting started and access/Onboarding and first work/Resume a saved draft", + component: Resume, + parameters: { layout: "fullscreen" }, + beforeEach: ({ parameters }) => { + seedOnboardingDraft(); + if (parameters.invalid) + localStorage.setItem(ONBOARDING_STORAGE_KEY, "invalid JSON"); + if (parameters.otherCompany) + localStorage.setItem( + ONBOARDING_STORAGE_KEY, + JSON.stringify({ + step: 3, + createdCompanyId: "unavailable-company", + agentName: "Another account's draft", + }), + ); + return clearOnboardingDraft; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const SavedAgentName: Story = { + play: async () => { + await waitFor( + () => + expect( + within(document.body).getByDisplayValue("Darnold"), + ).toBeInTheDocument(), + { timeout: 10000 }, + ); + }, +}; +export const DraftAfterRemount: Story = { + play: async ({ canvasElement }) => { + const body = within(document.body); + const input = await body.findByDisplayValue( + "Darnold", + {}, + { timeout: 10000 }, + ); + await userEvent.clear(input); + await userEvent.click(input); + await userEvent.paste("Release reviewer"); + await waitFor(() => + expect( + JSON.parse(localStorage.getItem(ONBOARDING_STORAGE_KEY) ?? "{}") + .agentName, + ).toBe("Release reviewer"), + ); + await userEvent.click( + within(canvasElement).getByRole("button", { name: "Reopen saved draft" }), + ); + await body.findByDisplayValue("Release reviewer"); + }, +}; +export const MalformedDraft: Story = { parameters: { invalid: true } }; +export const UnavailableCompanyDraft: Story = { + parameters: { otherCompany: true }, + play: async () => { + const body = within(document.body); + await expect(await body.findByTestId("onboarding-wizard")).toBeVisible(); + const name = await body.findByLabelText("Agent name"); + await waitFor(() => expect(name).toBeVisible(), { timeout: 10000 }); + await expect(name).toHaveValue(""); + await waitFor(() => + expect( + body.queryByDisplayValue("Another account's draft"), + ).not.toBeInTheDocument(), + ); + }, +}; diff --git a/ui/storybook/stories/pipelines/learnings.stories.tsx b/ui/storybook/stories/pipelines/learnings.stories.tsx new file mode 100644 index 00000000000..66dc7bbd198 --- /dev/null +++ b/ui/storybook/stories/pipelines/learnings.stories.tsx @@ -0,0 +1,40 @@ +import { within } from "storybook/test"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Routes, Route } from "@/lib/router"; +import { Learnings } from "@/pages/Pipelines"; +import { installPipelineFixture } from "../../support/capability-gaps/pipelines"; +import type { FixtureState } from "../../support/capability-gaps/api"; +function Host() { + return ; +} +const meta = { + id: "feature-pipeline-learnings", + title: + "Automation and structured work/Pipelines, review queues, and learnings/Learnings", + component: Host, + parameters: { + layout: "padded", + initialEntries: ["/PAP/learnings"], + docs: { + description: { + component: + "Shipped Learnings with isolated API receipts. Guards, durable transitions, automation execution and learning provenance require production tests.", + }, + }, + }, + beforeEach: ({ parameters }) => + installPipelineFixture(parameters.fixtureState as FixtureState).cleanup, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Ready: Story = { + play: async ({ canvasElement }) => { + await within(canvasElement).findByText(/Release reviewer approved/); + }, +}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Empty: Story = { parameters: { fixtureState: "empty" } }; +export const Failed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; diff --git a/ui/storybook/stories/pipelines/pipeline-board.stories.tsx b/ui/storybook/stories/pipelines/pipeline-board.stories.tsx new file mode 100644 index 00000000000..fe43f735422 --- /dev/null +++ b/ui/storybook/stories/pipelines/pipeline-board.stories.tsx @@ -0,0 +1,44 @@ +import { within } from "storybook/test"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Routes, Route } from "@/lib/router"; +import { Pipelines } from "@/pages/Pipelines"; +import { installPipelineFixture } from "../../support/capability-gaps/pipelines"; +import type { FixtureState } from "../../support/capability-gaps/api"; +function Host() { + return ( + + } /> + + ); +} +const meta = { + id: "feature-pipeline-board", + title: + "Automation and structured work/Pipelines, review queues, and learnings/Items", + component: Host, + parameters: { + layout: "padded", + initialEntries: ["/PAP/pipelines/pipeline-gap"], + docs: { + description: { + component: + "Shipped Pipelines with isolated API receipts. Guards, durable transitions, automation execution and learning provenance require production tests.", + }, + }, + }, + beforeEach: ({ parameters }) => + installPipelineFixture(parameters.fixtureState as FixtureState).cleanup, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Ready: Story = { + play: async ({ canvasElement }) => { + await within(canvasElement).findAllByText("Ship the release notes"); + }, +}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Empty: Story = { parameters: { fixtureState: "empty" } }; +export const Failed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; diff --git a/ui/storybook/stories/pipelines/review-queue.stories.tsx b/ui/storybook/stories/pipelines/review-queue.stories.tsx new file mode 100644 index 00000000000..4c31980cb0d --- /dev/null +++ b/ui/storybook/stories/pipelines/review-queue.stories.tsx @@ -0,0 +1,61 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, userEvent, within, waitFor } from "storybook/test"; +import { Routes, Route } from "@/lib/router"; +import { ReviewQueue } from "@/pages/Pipelines"; +import { installPipelineFixture } from "../../support/capability-gaps/pipelines"; +import type { FixtureState } from "../../support/capability-gaps/api"; +function Host() { + return ; +} +const meta = { + id: "feature-pipeline-review", + title: + "Automation and structured work/Pipelines, review queues, and learnings/Review queue", + component: Host, + parameters: { + layout: "padded", + initialEntries: ["/PAP/review-queue"], + docs: { + description: { + component: + "Shipped ReviewQueue with isolated API receipts. Guards, durable transitions, automation execution and learning provenance require production tests.", + }, + }, + }, + beforeEach: ({ parameters }) => + installPipelineFixture(parameters.fixtureState as FixtureState).cleanup, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Ready: Story = { + play: async ({ canvasElement }) => { + await within(canvasElement).findAllByText("Ship the release notes"); + }, +}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Empty: Story = { parameters: { fixtureState: "empty" } }; +export const Failed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; + +export const Approved: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await c.findByText("Ship the release notes"); + await userEvent.click(c.getByRole("button", { name: /^Approve$/ })); + await waitFor(() => + expect(c.queryByText("Ship the release notes")).not.toBeInTheDocument(), + ); + }, +}; +export const ApprovalFailed: Story = { + parameters: { fixtureState: "mutation-failure" }, + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await c.findByText("Ship the release notes"); + await userEvent.click(c.getByRole("button", { name: /^Approve$/ })); + await c.findByText("Needs attention"); + await c.findByText("Ship the release notes"); + }, +}; diff --git a/ui/storybook/stories/pipelines/stage-settings.stories.tsx b/ui/storybook/stories/pipelines/stage-settings.stories.tsx new file mode 100644 index 00000000000..3335f18d64a --- /dev/null +++ b/ui/storybook/stories/pipelines/stage-settings.stories.tsx @@ -0,0 +1,54 @@ +import { within } from "storybook/test"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Routes, Route } from "@/lib/router"; +import { PipelineSettings } from "@/pages/PipelineSettings"; +import { installPipelineFixture } from "../../support/capability-gaps/pipelines"; +import type { FixtureState } from "../../support/capability-gaps/api"; +function Host() { + return ( + + } + /> + + ); +} +const meta = { + id: "feature-pipeline-stages", + title: + "Automation and structured work/Pipelines, review queues, and learnings/Stage settings", + component: Host, + parameters: { + layout: "padded", + initialEntries: ["/PAP/pipelines/pipeline-gap/settings"], + docs: { + description: { + component: + "Shipped PipelineSettings with isolated API receipts. Guards, durable transitions, automation execution and learning provenance require production tests.", + }, + }, + }, + beforeEach: ({ parameters }) => + installPipelineFixture(parameters.fixtureState as FixtureState, { + emptyStages: parameters.fixtureState === "empty", + }).cleanup, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Ready: Story = { + play: async ({ canvasElement }) => { + await within(canvasElement).findByDisplayValue("Release review"); + }, +}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Empty: Story = { + parameters: { fixtureState: "empty" }, + play: async ({ canvasElement }) => { + await within(canvasElement).findByText("No stages configured."); + }, +}; +export const Failed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; diff --git a/ui/storybook/stories/plugins/plugin-configuration.stories.tsx b/ui/storybook/stories/plugins/plugin-configuration.stories.tsx new file mode 100644 index 00000000000..7a9dbe58da7 --- /dev/null +++ b/ui/storybook/stories/plugins/plugin-configuration.stories.tsx @@ -0,0 +1,75 @@ +import { PluginManager } from "@/pages/PluginManager"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Routes, Route } from "@/lib/router"; +import { PluginSettings } from "@/pages/PluginSettings"; +import { expect, userEvent, within } from "storybook/test"; +import { installPluginFixtures } from "../../support/capability-gaps/plugins"; +import type { FixtureState } from "../../support/capability-gaps/api"; +function Settings() { + return ( + + } + /> + } + /> + + ); +} +const meta = { + id: "feature-plugin-configuration", + title: "Apps, channels, and extensions/Plugins/Configuration", + component: Settings, + parameters: { + layout: "padded", + initialEntries: ["/PAP/plugins/plugin-gap/settings"], + }, + beforeEach: ({ parameters }) => + installPluginFixtures(parameters.fixtureState as FixtureState), +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Configured: Story = {}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Unavailable: Story = { parameters: { fixtureState: "failure" } }; +export const SaveConfiguration: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + const input = await c.findByDisplayValue("Release preview"); + await userEvent.clear(input); + await userEvent.type(input, "Reviewed release", { delay: 15 }); + await userEvent.click( + c.getByRole("button", { name: "Save Configuration" }), + ); + await c.findByText("Configuration saved."); + await expect(input).toHaveValue("Reviewed release"); + }, +}; +export const SaveFailed: Story = { + parameters: { fixtureState: "mutation-failure" }, + play: async ({ canvasElement }) => { + const c = within(canvasElement); + const input = await c.findByDisplayValue("Release preview"); + await userEvent.clear(input); + await userEvent.type(input, "Reviewed release", { delay: 15 }); + await userEvent.click( + c.getByRole("button", { name: "Save Configuration" }), + ); + await c.findByText("The request failed. Please try again."); + }, +}; +export const PermissionDenied: Story = { + parameters: { fixtureState: "mutation-permission" }, + play: async ({ canvasElement }) => { + const c = within(canvasElement); + const input = await c.findByDisplayValue("Release preview"); + await userEvent.type(input, " updated", { delay: 15 }); + await userEvent.click( + c.getByRole("button", { name: "Save Configuration" }), + ); + await c.findByText("You do not have permission to perform this action."); + }, +}; diff --git a/ui/storybook/stories/plugins/plugin-contributions.stories.tsx b/ui/storybook/stories/plugins/plugin-contributions.stories.tsx new file mode 100644 index 00000000000..18c029588ff --- /dev/null +++ b/ui/storybook/stories/plugins/plugin-contributions.stories.tsx @@ -0,0 +1,105 @@ +import { PluginSettings } from "@/pages/PluginSettings"; +import { PluginManager } from "@/pages/PluginManager"; +import { installPluginFixtures } from "../../support/capability-gaps/plugins"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Routes, Route } from "@/lib/router"; +import { PluginPage } from "@/pages/PluginPage"; +import { registerPluginReactComponent } from "@/plugins/slots"; +import { within } from "storybook/test"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +function Contribution() { + return ( + + } + /> + } + /> + } /> + + ); +} +function FixturePluginContent() { + return

Isolated plugin contribution

; +} +const meta = { + id: "feature-plugin-contributions", + title: "Apps, channels, and extensions/Plugins/Contributed routes", + component: Contribution, + parameters: { + layout: "padded", + initialEntries: ["/PAP/plugins/contribution-gap"], + }, + beforeEach: ({ parameters }) => { + registerPluginReactComponent( + "storybook.contribution", + "FixturePluginContent", + FixturePluginContent, + ); + const baseCleanup = installPluginFixtures( + "ready", + false, + "contribution-gap", + ); + const fixture = prepareFixtureApi( + [ + { + path: "/api/plugins/ui-contributions", + read: () => + parameters.empty + ? [] + : [ + { + pluginId: "contribution-gap", + pluginKey: "storybook.contribution", + displayName: "Preview contribution", + version: "1.0.0", + uiEntryFile: "fixture.js", + slots: [ + { + type: "page", + id: "fixture-page", + displayName: "Preview", + exportName: "FixturePluginContent", + routePath: "fixture", + }, + ], + launchers: [], + }, + ], + }, + ], + parameters.fixtureState as FixtureState, + ); + return () => { + fixture.cleanup(); + baseCleanup(); + }; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const MountedContribution: Story = { + play: async ({ canvasElement }) => { + await within(canvasElement).findByText("Isolated plugin contribution"); + }, +}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const MissingContribution: Story = { parameters: { empty: true } }; +export const DiscoveryFailure: Story = { + parameters: { + fixtureState: "failure", + docs: { + description: { + story: + "Current production PluginPage retains its Loading state after contribution discovery fails. This exposes a product error-state gap; it is not a passing failure UX.", + }, + }, + }, +}; diff --git a/ui/storybook/stories/plugins/plugin-management.stories.tsx b/ui/storybook/stories/plugins/plugin-management.stories.tsx new file mode 100644 index 00000000000..2436fe9bad9 --- /dev/null +++ b/ui/storybook/stories/plugins/plugin-management.stories.tsx @@ -0,0 +1,89 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, userEvent, within } from "storybook/test"; +import { PluginManager } from "@/pages/PluginManager"; +import { ToastViewport } from "@/components/ToastViewport"; +import { installPluginFixtures } from "../../support/capability-gaps/plugins"; +import type { FixtureState } from "../../support/capability-gaps/api"; +function Plugins() { + return ( + <> + + + + ); +} +const meta = { + id: "feature-plugin-management", + title: "Apps, channels, and extensions/Plugins/Installation and lifecycle", + component: Plugins, + parameters: { layout: "padded" }, + beforeEach: ({ parameters }) => + installPluginFixtures( + parameters.fixtureState as FixtureState, + Boolean(parameters.empty), + ), +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Installed: Story = {}; +export const Empty: Story = { parameters: { empty: true } }; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Failed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; +async function install() { + const c = within(document.body); + await userEvent.click( + await c.findByRole("button", { name: "Install Plugin" }), + ); + await userEvent.type( + await c.findByRole("textbox"), + "@paperclipai/storybook-fixture", + { delay: 15 }, + ); + await userEvent.click(c.getByRole("button", { name: "Install" })); +} +export const InstallPackage: Story = { + parameters: { empty: true }, + play: async () => { + await install(); + await within(document.body).findByText("Plugin installed successfully"); + await within(document.body).findByText("Preview plugin"); + }, +}; +export const Installing: Story = { + parameters: { empty: true, fixtureState: "saving" }, + play: async () => { + await install(); + await expect( + await within(document.body).findByRole("button", { + name: "Installing...", + }), + ).toBeDisabled(); + }, +}; +export const InstallFailed: Story = { + parameters: { empty: true, fixtureState: "mutation-failure" }, + play: async () => { + await install(); + await within(document.body).findByText("Failed to install plugin"); + }, +}; +export const DisablePlugin: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click(await c.findByTitle("Disable")); + await within(document.body).findByText("Plugin disabled"); + await c.findByTitle("Enable"); + }, +}; +export const UninstallPlugin: Story = { + play: async ({ canvasElement }) => { + await userEvent.click(await within(canvasElement).findByTitle("Uninstall")); + const c = within(document.body); + await userEvent.click(c.getByRole("button", { name: "Uninstall" })); + await c.findByText("Plugin uninstalled successfully"); + await expect(c.queryByText("Preview plugin")).not.toBeInTheDocument(); + }, +}; diff --git a/ui/storybook/stories/recovery/recovery-gap-states.stories.tsx b/ui/storybook/stories/recovery/recovery-gap-states.stories.tsx new file mode 100644 index 00000000000..719f8e690df --- /dev/null +++ b/ui/storybook/stories/recovery/recovery-gap-states.stories.tsx @@ -0,0 +1,178 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { IssueRecoveryAction } from "@paperclipai/shared"; +import { expect, userEvent, within, waitFor } from "storybook/test"; +import { IssueRecoveryActionCard } from "@/components/IssueRecoveryActionCard"; +import { + DispositionRecoveryNotice, + DispositionRecoveryProvider, +} from "@/components/DispositionRecoveryNotice"; +import { storybookAgents } from "../../fixtures/paperclipData"; +const agent = storybookAgents[0]; +const action: IssueRecoveryAction = { + id: "recovery-gap", + companyId: agent.companyId, + sourceIssueId: "task-gap", + recoveryIssueId: null, + kind: "deliberate_wait_without_target", + status: "active", + ownerType: "board", + ownerAgentId: null, + ownerUserId: null, + previousOwnerAgentId: agent.id, + returnOwnerAgentId: agent.id, + cause: "unchanged_source_state_exhausted", + fingerprint: "storybook-recovery-gap", + evidence: { + summary: + "Automatic recovery exhausted two attempts without a durable next step.", + }, + nextAction: "Inspect the outcome before deciding whether to retry.", + wakePolicy: { type: "board_escalation" }, + monitorPolicy: null, + attemptCount: 2, + maxAttempts: 2, + timeoutAt: null, + lastAttemptAt: "2026-10-12T12:00:00Z", + outcome: null, + resolutionNote: null, + resolvedAt: null, + createdAt: "2026-10-12T12:00:00Z", + updatedAt: "2026-10-12T12:00:00Z", +}; +function Recovery({ + state = "ready", +}: { + state?: "ready" | "blocked" | "failure" | "saving" | "historical"; +}) { + const [resolved, setResolved] = useState(false); + return ( + { + if (state === "saving") return new Promise(() => {}); + if (state === "failure") + throw new Error("The recovery decision could not be saved."); + setResolved(true); + }, + }} + > + + + ); +} +const meta = { + id: "feature-recovery-gap-states", + title: "Projects and execution/Recovery/Exhaustion and user decisions", + component: Recovery, + parameters: { + layout: "padded", + docs: { + description: { + component: + "Shipped recovery notice/card. Decisions alter local fixture state only. Bounded attempts, unknown-side-effect replay and real continuation require production tests.", + }, + }, + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Exhausted: Story = { + play: async ({ canvasElement }) => { + await within(canvasElement).findByRole("button", { name: "Retry agent" }); + }, +}; +export const PermissionBlocked: Story = { + args: { state: "blocked" }, + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await expect( + await c.findByRole("button", { name: "Retry agent" }), + ).toBeDisabled(); + await c.findByText("You do not have permission to retry this task."); + }, +}; +export const RetryFailed: Story = { + args: { state: "failure" }, + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click( + await c.findByRole("button", { name: "Retry agent" }), + ); + await c.findByRole("alert"); + }, +}; +export const RetryPending: Story = { + args: { state: "saving" }, + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click( + await c.findByRole("button", { name: "Retry agent" }), + ); + const retry = await c.findByRole("button", { name: "Requesting retry…" }); + await waitFor(() => expect(retry).toBeDisabled()); + }, +}; +export const RetryAccepted: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click( + await c.findByRole("button", { name: "Retry agent" }), + ); + await c.findByRole("heading", { name: "Retry requested" }); + }, +}; +export const NoActiveRecovery: Story = { + args: { state: "historical" }, + play: async ({ canvasElement }) => { + await within(canvasElement).findByRole("heading", { + name: "Agent needed attention", + }); + }, +}; +export const UnknownOutcome: Story = { + render: () => ( + + ), + play: async ({ canvasElement }) => { + await within(canvasElement).findByText( + "The external request may have completed. Inspect the provider before replaying it.", + ); + }, +}; diff --git a/ui/storybook/stories/routines/run-now.stories.tsx b/ui/storybook/stories/routines/run-now.stories.tsx new file mode 100644 index 00000000000..390245e97da --- /dev/null +++ b/ui/storybook/stories/routines/run-now.stories.tsx @@ -0,0 +1,140 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Routes, Route } from "@/lib/router"; +import { RoutineDetail } from "@/pages/RoutineDetail"; +import { ToastViewport } from "@/components/ToastViewport"; +import { expect, userEvent, within, waitFor } from "storybook/test"; +import { baseRoutine } from "../../fixtures/routineWebhooks"; +import { storybookIssues } from "../../fixtures/paperclipData"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +let submitted: Record | null = null; +function RunRoutine() { + return ( + <> + + } + /> + + + + ); +} +const meta = { + id: "feature-routine-run-now", + title: + "Automation and structured work/Routines, schedules, and triggers/Run now", + component: RunRoutine, + parameters: { + layout: "padded", + initialEntries: ["/PAP/routines/routine-gap"], + }, + beforeEach: ({ parameters }) => { + submitted = null; + let tasks: unknown[] = []; + return prepareFixtureApi( + [ + { + path: "/api/routines/routine-gap", + read: () => ({ + ...baseRoutine, + id: "routine-gap", + title: "Review a release", + variables: [ + { + name: "environment", + label: "Environment", + type: "text", + required: true, + defaultValue: "", + options: [], + }, + ], + }), + }, + { path: "/api/routines/routine-gap/runs", read: () => [] }, + { path: "/api/companies/company-storybook/issues", read: () => tasks }, + { + path: "/api/routines/routine-gap/run", + method: "POST", + read: ({ body }) => { + submitted = body; + tasks = [ + { + ...storybookIssues[0], + id: "run-task-gap", + title: "Review a release in staging", + originKind: "routine_execution", + originId: "routine-gap", + }, + ]; + return { + id: "routine-run-gap", + companyId: "company-storybook", + routineId: "routine-gap", + status: "enqueued", + linkedIssueId: "run-task-gap", + }; + }, + }, + ], + parameters.fixtureState as FixtureState, + ).cleanup; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Overview: Story = {}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Failed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; +async function openRun() { + const c = within(document.body); + await userEvent.click(await c.findByRole("button", { name: "Run now" })); + return c; +} +export const RequiredInput: Story = { + play: async () => { + const c = await openRun(); + await expect(c.getByRole("button", { name: "Run routine" })).toBeDisabled(); + }, +}; +async function run() { + const c = await openRun(); + await userEvent.type( + within(c.getByRole("dialog")).getByRole("textbox"), + "staging", + { delay: 15 }, + ); + await userEvent.click(c.getByRole("button", { name: "Run routine" })); +} +export const Started: Story = { + play: async () => { + await run(); + await within(document.body).findByText("Routine run started"); + await waitFor(() => + expect(submitted?.variables).toEqual({ environment: "staging" }), + ); + }, +}; +export const Starting: Story = { + parameters: { fixtureState: "saving" }, + play: async () => { + await run(); + await expect( + await within(document.body).findByRole("button", { name: "Running..." }), + ).toBeDisabled(); + }, +}; +export const StartFailed: Story = { + parameters: { fixtureState: "mutation-failure" }, + play: async () => { + await run(); + await within(document.body).findByText("Routine run failed"); + }, +}; diff --git a/ui/storybook/stories/secrets/proposal-review.stories.tsx b/ui/storybook/stories/secrets/proposal-review.stories.tsx new file mode 100644 index 00000000000..71713cc0c3c --- /dev/null +++ b/ui/storybook/stories/secrets/proposal-review.stories.tsx @@ -0,0 +1,143 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { SecretProposalView } from "@paperclipai/shared"; +import { expect, userEvent, within, waitFor } from "storybook/test"; +import { ProposalsTab } from "@/pages/secrets/ProposalsTab"; +import { ToastViewport } from "@/components/ToastViewport"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +const proposal: SecretProposalView = { + id: "proposal-gap", + companyId: "company-storybook", + kind: "secret", + status: "pending", + justification: "The deploy task needs access to the GitHub API.", + proposedName: "dev/github/token", + proposedKey: "token", + proposedDescription: null, + valueFingerprintSha256: "abcdef0123456789abcdef", + valueLength: 40, + secretId: null, + secretName: null, + secretProposalId: null, + secretProposalName: null, + targetType: null, + target: null, + configPath: null, + proposedBy: { id: "agent-coder", name: "Codex Coder", icon: "code" }, + originIssue: { id: "task-gap", key: "PAP-42", title: "Deploy preview" }, + originRunId: "run-gap", + expiresAt: "2099-10-13T12:00:00Z", + createdAt: "2026-10-12T12:00:00Z", + resolvedByUserId: null, + resolvedAt: null, + resolutionReason: null, + createdSecretId: null, + appliedBindingConfigPath: null, + viewerCanApprove: true, + approveBlockReason: null, +}; +let rows: SecretProposalView[] = []; +function ProposalReview() { + return ( + <> + + + + ); +} +const meta = { + id: "feature-secret-proposal-review", + title: "Apps, channels, and extensions/Secrets and proposals/Proposal review", + component: ProposalReview, + parameters: { layout: "padded" }, + beforeEach: ({ parameters }) => { + rows = + parameters.fixtureState === "empty" + ? [] + : [ + { + ...proposal, + viewerCanApprove: !parameters.blocked, + approveBlockReason: parameters.blocked + ? "Only an authorized secret administrator can approve this proposal." + : null, + }, + ]; + return prepareFixtureApi( + [ + { + path: "/api/companies/company-storybook/secret-proposals", + read: () => rows, + }, + { + path: /^\/api\/companies\/company-storybook\/secret-proposals\/proposal-gap\/(approve|reject)$/, + method: "POST", + read: ({ url, body }) => { + const result = { + ...rows[0], + status: url.pathname.endsWith("approve") + ? ("approved" as const) + : ("rejected" as const), + resolutionReason: String(body.reason ?? ""), + resolvedAt: "2026-10-13T12:00:00Z", + }; + rows = []; + return result; + }, + }, + ], + parameters.fixtureState as FixtureState, + ).cleanup; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Pending: Story = {}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Empty: Story = { parameters: { fixtureState: "empty" } }; +export const Failed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionBlocked: Story = { + parameters: { blocked: true }, + play: async ({ canvasElement }) => { + await expect( + await within(canvasElement).findByRole("button", { name: "Approve" }), + ).toBeDisabled(); + }, +}; +export const RejectProposal: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click(await c.findByRole("button", { name: "Reject" })); + const body = within(document.body); + await userEvent.type( + await body.findByRole("textbox"), + "The task can use the existing managed connection.", + { delay: 15 }, + ); + await userEvent.click( + within(body.getByRole("dialog")).getByRole("button", { name: "Reject" }), + ); + await waitFor(() => expect(rows).toHaveLength(0)); + await body.findByText("Proposal rejected"); + }, +}; +export const RejectFailed: Story = { + parameters: { fixtureState: "mutation-failure" }, + play: async ({ canvasElement }) => { + await userEvent.click( + await within(canvasElement).findByRole("button", { name: "Reject" }), + ); + const body = within(document.body); + await userEvent.type( + await body.findByRole("textbox"), + "Use the existing connection.", + { delay: 15 }, + ); + await userEvent.click( + within(body.getByRole("dialog")).getByRole("button", { name: "Reject" }), + ); + await body.findByText("The request failed. Please try again."); + }, +}; diff --git a/ui/storybook/stories/skills/skill-test-runs.stories.tsx b/ui/storybook/stories/skills/skill-test-runs.stories.tsx new file mode 100644 index 00000000000..2a868c40fc7 --- /dev/null +++ b/ui/storybook/stories/skills/skill-test-runs.stories.tsx @@ -0,0 +1,177 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, userEvent, within } from "storybook/test"; +import { Routes, Route } from "@/lib/router"; +import { SkillStudio } from "@/pages/SkillStudio"; +import { makeSkill } from "../../support/capability-gaps/skills"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +function Studio() { + return ( + + } /> + + ); +} +const meta = { + id: "feature-skill-test-runs", + title: "Agents and reusable capabilities/Skills and Skill Studio/Test runs", + component: Studio, + parameters: { + layout: "fullscreen", + initialEntries: ["/PAP/skills/studio/skill-gap"], + }, + beforeEach: ({ parameters }) => { + const skill = makeSkill(); + const run = { + id: "run-gap", + companyId: skill.companyId, + skillId: skill.id, + inputId: "input-gap", + inputSnapshot: "Review this release.", + skillVersionId: "version-gap", + agentId: "agent-codex", + agentConfigSnapshot: {}, + issueId: "task-gap", + templateId: null, + templateName: null, + templateBody: null, + renderedTemplateBody: null, + harnessIssueDescription: "Review this release.", + status: parameters.runStatus ?? "succeeded", + outputDocumentKey: "result", + outputSnapshot: + parameters.runStatus === "running" || parameters.runStatus === "queued" + ? null + : "The release checks passed.", + error: + parameters.runStatus === "failed" + ? "The provider could not complete the test." + : null, + deletedAt: null, + supersededAt: null, + harnessIssueExpiresAt: null, + harnessIssueDeletedAt: null, + createdAt: "2026-10-12T12:00:00Z", + updatedAt: "2026-10-12T12:01:00Z", + cost: { + costCents: 2, + inputTokens: 100, + cachedInputTokens: 0, + outputTokens: 50, + }, + taskExpired: false, + }; + let runs = parameters.fixtureState === "empty" ? [] : [run]; + const detail = () => ({ + ...run, + skillVersion: { + id: "version-gap", + companyId: skill.companyId, + companySkillId: skill.id, + revisionNumber: 1, + label: "Release check", + releaseId: null, + releaseName: null, + releasedAt: null, + fileInventory: [], + authorAgentId: null, + authorUserId: "user-storybook", + createdAt: run.createdAt, + }, + outputBody: run.outputSnapshot, + harnessContent: { + available: false, + unavailableReason: "expired", + documents: [], + attachments: [], + workProducts: [], + }, + harnessIssue: null, + documents: [], + interactions: [], + artifacts: [], + }); + return prepareFixtureApi( + [ + { + path: "/api/companies/company-storybook/skills", + read: () => [skill], + }, + { + path: "/api/companies/company-storybook/skills/skill-gap", + read: () => skill, + }, + { + path: "/api/companies/company-storybook/skills/skill-gap/files", + read: () => ({ + skillId: skill.id, + path: "SKILL.md", + content: skill.markdown, + language: "markdown", + editable: true, + }), + }, + { + path: "/api/companies/company-storybook/skills/skill-gap/test-inputs", + read: () => + parameters.fixtureState === "empty" + ? [] + : [ + { + id: "input-gap", + companyId: skill.companyId, + skillId: skill.id, + name: "Release review", + content: "Review this release.", + createdBy: null, + deletedAt: null, + createdAt: run.createdAt, + updatedAt: run.updatedAt, + }, + ], + }, + { + path: "/api/companies/company-storybook/skill-test-run-templates", + read: () => [], + }, + { + path: "/api/companies/company-storybook/skills/skill-gap/test-runs", + read: ({ method }) => { + if (method === "POST") { + const queued = { ...run, id: "run-queued", status: "queued" }; + runs = [queued, ...runs]; + return queued; + } + return runs; + }, + }, + { + path: "/api/companies/company-storybook/skills/skill-gap/test-runs/run-gap", + read: detail, + }, + ], + parameters.fixtureState as FixtureState, + ).cleanup; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const CompletedTest: Story = {}; +export const Queued: Story = { parameters: { runStatus: "queued" } }; +export const Running: Story = { parameters: { runStatus: "running" } }; +export const TestFailed: Story = { parameters: { runStatus: "failed" } }; +export const Empty: Story = { parameters: { fixtureState: "empty" } }; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const LoadFailed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; + +export const ResultDetails: Story = { + parameters: { initialEntries: ["/PAP/skills/studio/skill-gap?run=run-gap"] }, + play: async ({ canvasElement }) => { + await within(canvasElement).findByText("The release checks passed."); + }, +}; diff --git a/ui/storybook/stories/status-cards/status-card-states.stories.tsx b/ui/storybook/stories/status-cards/status-card-states.stories.tsx new file mode 100644 index 00000000000..8e38f4c653a --- /dev/null +++ b/ui/storybook/stories/status-cards/status-card-states.stories.tsx @@ -0,0 +1,244 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Routes, Route } from "@/lib/router"; +import { StatusCards } from "@/pages/StatusCards"; +import { expect, userEvent, within, waitFor } from "storybook/test"; +import { makeStatusCard } from "../../support/capability-gaps/status-cards"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +function StatusCardHost() { + return ( + + } /> + + ); +} +const meta = { + id: "feature-status-card-states", + title: + "Automation and structured work/Status cards/Authoring, updates and archive", + component: StatusCardHost, + parameters: { layout: "padded", initialEntries: ["/status"] }, + beforeEach: ({ parameters }) => { + let card = makeStatusCard({ + state: parameters.cardState ?? "active", + generatingIssueId: + parameters.cardState === "compiling" ? "summary-task-gap" : null, + ...(parameters.cardState === "compiling" + ? { queryCompiledAt: null, lastGeneratedAt: null } + : {}), + failureReason: + parameters.cardState === "error" + ? "The summarizer could not finish this update." + : null, + summaryBody: + parameters.cardState === "compiling" + ? null + : "Build and tests passed. Provider qualification remains pending.", + pendingChangeCount: parameters.stale ? 3 : 0, + archivedAt: parameters.archived ? "2026-10-12T12:00:00Z" : null, + }); + let rows = parameters.fixtureState === "empty" ? [] : [card]; + return prepareFixtureApi( + [ + { + path: "/api/companies/company-storybook/status-cards", + read: ({ method, url, body }) => { + if (method === "POST") { + card = makeStatusCard({ + title: "New review card", + generatingIssueId: "summary-task-gap", + queryCompiledAt: null, + state: "compiling", + interestPrompt: String(body.interestPrompt), + summaryBody: null, + lastGeneratedAt: null, + }); + rows = [card]; + return card; + } + return rows.filter( + (row) => + Boolean(row.archivedAt) === + (url.searchParams.get("archived") === "true"), + ); + }, + }, + { + path: "/api/status-cards/card-gap", + read: ({ method, body }) => { + if (method === "PATCH") { + card = { + ...card, + archivedAt: body.archived ? "2026-10-13T12:00:00Z" : null, + pendingChangeCount: body.archived ? 0 : 1, + }; + rows = [card]; + } + return card; + }, + }, + { + path: /^\/api\/status-cards\/card-gap\/(refresh|recompile)$/, + method: "POST", + read: () => { + card = { + ...card, + pendingChangeCount: 0, + summaryBody: "All qualification checks passed. Ready for review.", + lastGeneratedAt: "2026-10-13T12:00:00Z", + }; + rows = [card]; + return card; + }, + }, + { path: "/api/status-cards/card-gap/updates", read: () => [] }, + { + path: "/api/status-cards/card-gap/summary-revisions", + read: () => [], + }, + { + path: "/api/status-cards/card-gap/dry-run", + read: () => ({ + cardId: card.id, + queryVersion: 1, + queries: [], + mentionedIssues: [], + }), + }, + ], + parameters.fixtureState as FixtureState, + ).cleanup; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const CurrentSummary: Story = { + play: async ({ canvasElement }) => { + await within(canvasElement).findByText( + "Build and tests passed. Provider qualification remains pending.", + ); + }, +}; +export const Loading: Story = { parameters: { fixtureState: "loading" } }; +export const Empty: Story = { parameters: { fixtureState: "empty" } }; +export const LoadFailed: Story = { parameters: { fixtureState: "failure" } }; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, +}; +export const SettingUp: Story = { parameters: { cardState: "compiling" } }; +export const GenerationFailed: Story = { parameters: { cardState: "error" } }; +export const StaleSummary: Story = { parameters: { stale: true } }; +async function create() { + const c = within(document.body); + const button = (await c.findAllByRole("button", { name: "New card" }))[0]; + await waitFor(() => expect(button).toBeEnabled()); + await userEvent.click(button); + await userEvent.type( + await c.findByRole("textbox", { + name: "What do you want to keep an eye on?", + }), + "Watch the release checks and summarize pending qualification.", + { delay: 15 }, + ); + const createButton = c.getByRole("button", { name: "Create card" }); + await waitFor(() => expect(createButton).toBeEnabled()); + await userEvent.click(createButton); +} +export const Created: Story = { + parameters: { fixtureState: "empty" }, + play: async () => { + await create(); + await within(document.body).findByText("New review card"); + }, +}; +export const CreateFailed: Story = { + parameters: { fixtureState: "mutation-failure" }, + play: async () => { + await create(); + await within(document.body).findByText( + "The request failed. Please try again.", + ); + }, +}; +export const Refreshed: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click( + await c.findByRole("button", { name: "Refresh card" }), + ); + await c.findByText("All qualification checks passed. Ready for review."); + }, +}; +export const RefreshFailed: Story = { + parameters: { fixtureState: "mutation-failure" }, + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click( + await c.findByRole("button", { name: "Refresh card" }), + ); + await c.findByText("The request failed. Please try again."); + }, +}; +export const Refreshing: Story = { + parameters: { fixtureState: "saving" }, + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click( + await c.findByRole("button", { name: "Refresh card" }), + ); + await expect( + c.getByRole("button", { name: "Refresh card" }), + ).toBeDisabled(); + }, +}; +async function action(canvasElement: HTMLElement, name: string) { + await userEvent.click( + await within(canvasElement).findByRole("button", { name: "Card actions" }), + ); + await userEvent.click( + await within(document.body).findByRole("menuitem", { name }), + ); +} +export const Inspection: Story = { + play: async ({ canvasElement }) => { + await action(canvasElement, "Open detail"); + await within(document.body).findByRole("dialog"); + }, +}; +export const Archive: Story = { + play: async ({ canvasElement }) => { + await action(canvasElement, "Archive"); + await waitFor(() => + expect( + within(canvasElement).queryByText( + "Build and tests passed. Provider qualification remains pending.", + ), + ).not.toBeInTheDocument(), + ); + }, +}; +export const Archived: Story = { + parameters: { archived: true }, + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click( + await c.findByRole("button", { name: "Show archived (1)" }), + ); + await c.findByRole("button", { name: "Restore" }); + }, +}; +export const Restored: Story = { + parameters: { archived: true }, + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click( + await c.findByRole("button", { name: "Show archived (1)" }), + ); + await userEvent.click(c.getByRole("button", { name: "Restore" })); + await c.findByText( + "Build and tests passed. Provider qualification remains pending.", + ); + }, +}; diff --git a/ui/storybook/stories/steering/composer-draft-recovery.stories.tsx b/ui/storybook/stories/steering/composer-draft-recovery.stories.tsx new file mode 100644 index 00000000000..9d920ac52c6 --- /dev/null +++ b/ui/storybook/stories/steering/composer-draft-recovery.stories.tsx @@ -0,0 +1,95 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { TaskChatComposer } from "@/components/task-chat/TaskChatComposer"; +import { Button } from "@/components/ui/button"; +import { + saveDraft, + loadDraft, + clearDraft, + saveDraftSubmission, + loadDraftSubmission, +} from "@/lib/composer-draft"; +import { expect, userEvent, within, waitFor } from "storybook/test"; +const first = "paperclip:storybook:gap-draft-a", + second = "paperclip:storybook:gap-draft-b"; +function resetFixtureDraft(key: string) { + clearDraft(key, loadDraftSubmission(key)?.attemptId); +} +function Drafts({ fail = false }: { fail?: boolean }) { + const [task, setTask] = useState(first); + return ( + <> + + { + if (fail) throw new Error("The message could not be saved."); + }} + /> + + ); +} +const meta = { + id: "feature-composer-draft-recovery", + title: "Tasks and conversations/Steering and queued messages/Draft recovery", + component: Drafts, + parameters: { layout: "padded" }, + beforeEach: ({ parameters }) => { + resetFixtureDraft(first); + resetFixtureDraft(second); + if (!parameters.empty) + saveDraft(first, "Review the release before publishing."); + saveDraft(second, "Check provider qualification."); + if (parameters.uncertain) + saveDraftSubmission(first, { + attemptId: "00000000-0000-4000-8000-000000000001", + reviewed: false, + }); + return () => { + resetFixtureDraft(first); + resetFixtureDraft(second); + }; + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Restored: Story = {}; +export const Empty: Story = { parameters: { empty: true } }; +export const UncertainSubmission: Story = { parameters: { uncertain: true } }; +export const SwitchAndRestore: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + const editor = await c.findByRole("textbox"); + await userEvent.click(editor); + await userEvent.paste(" Additional evidence."); + await waitFor( + () => expect(loadDraft(first)).toContain("Additional evidence."), + { timeout: 3000 }, + ); + await userEvent.click(c.getByRole("button", { name: "Switch task" })); + await c.findByText("Check provider qualification."); + await userEvent.click(c.getByRole("button", { name: "Switch task" })); + await c.findByText(/Additional evidence/); + }, +}; +export const SendFailedKeepsDraft: Story = { + args: { fail: true }, + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click(await c.findByRole("button", { name: /Send/ })); + await waitFor(() => + expect(loadDraft(first)).toContain("Review the release"), + ); + }, +}; +export const SentClearsDraft: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click(await c.findByRole("button", { name: /Send/ })); + await waitFor(() => expect(loadDraft(first)).toBe("")); + }, +}; diff --git a/ui/storybook/stories/steering/durable-response-queue.stories.tsx b/ui/storybook/stories/steering/durable-response-queue.stories.tsx new file mode 100644 index 00000000000..2b68d243216 --- /dev/null +++ b/ui/storybook/stories/steering/durable-response-queue.stories.tsx @@ -0,0 +1,173 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { IssueQueuedCommentQueue } from "@paperclipai/shared"; +import { expect, userEvent, within, waitFor } from "storybook/test"; +import { TaskChatQueuedMessages } from "@/components/task-chat/TaskChatQueuedMessages"; +const initialQueue: IssueQueuedCommentQueue = { + issueId: "task-gap", + queueId: "response-gap", + state: "deferred", + targetRunId: "run-gap", + revision: "rev-1", + protocol: "paperclip_runner_v1", + steeringDisposition: "available", + entries: [ + { + position: 0, + canEdit: false, + canDiscard: false, + source: { + kind: "interaction", + interactionId: "approval-gap", + interactionKind: "request_confirmation", + requiresFreshSession: true, + }, + comment: { + id: "response-comment", + companyId: "company-storybook", + issueId: "task-gap", + authorType: "user", + authorAgentId: null, + authorUserId: "user-storybook", + body: "Accepted: Review the migration\n\nApproved revision v2.", + presentation: null, + metadata: null, + createdAt: new Date("2026-10-12T12:00:00Z"), + updatedAt: new Date("2026-10-12T12:00:00Z"), + }, + }, + ], +}; +function ResponseQueue({ + state = "ready", +}: { + state?: "ready" | "empty" | "waiting" | "failure" | "saving" | "blocked"; +}) { + const [queue, setQueue] = useState({ + ...initialQueue, + queueId: state === "blocked" ? null : initialQueue.queueId, + entries: state === "empty" ? [] : initialQueue.entries, + executionWait: + state === "waiting" + ? { + reason: "remote_cleanup", + message: + "Waiting for the previous environment to stop. Your message will start automatically.", + } + : undefined, + }); + const deliver = async () => { + if (state === "failure") + throw new Error("The response could not be delivered."); + if (state === "saving") return new Promise(() => {}); + setQueue((current) => ({ + ...current, + entries: [], + executionWait: undefined, + revision: "rev-2", + })); + }; + return ( + { + throw new Error("Accepted responses cannot be edited."); + }} + onReorder={async () => { + throw new Error("Accepted responses cannot be reordered."); + }} + onSteer={deliver} + onInterrupt={deliver} + onDiscard={async () => { + throw new Error("Accepted responses cannot be discarded."); + }} + /> + ); +} +const meta = { + id: "feature-durable-response-queue", + title: + "Tasks and conversations/Steering and queued messages/Accepted response delivery", + component: ResponseQueue, + parameters: { + layout: "padded", + docs: { + description: { + component: + "Production queue with an immutable accepted response and remote-cleanup wait. Delivery uses local state; it does not prove persisted queues or runtime continuation.", + }, + }, + }, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const AcceptedResponse: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await c.findByText("Accepted: Review the migration"); + await expect( + c.getByRole("button", { name: /Reorder queued message/ }), + ).toBeDisabled(); + }, +}; +export const WaitingForCleanup: Story = { + args: { state: "waiting" }, + play: async ({ canvasElement }) => { + await within(canvasElement).findByText( + "Waiting for the previous environment to stop. Your message will start automatically.", + ); + }, +}; +export const AdmissionUnavailable: Story = { + args: { state: "blocked" }, + play: async ({ canvasElement }) => { + await expect( + await within(canvasElement).findByRole("button", { name: "Interrupt" }), + ).toBeDisabled(); + }, +}; +export const Empty: Story = { args: { state: "empty" } }; +export const DeliveryFailed: Story = { + args: { state: "failure" }, + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click(await c.findByRole("button", { name: "Interrupt" })); + await waitFor(() => + expect(c.getByText("Accepted: Review the migration")).toBeInTheDocument(), + ); + await c.findByText("Couldn’t interrupt. Message is still queued."); + }, +}; +export const Delivering: Story = { + args: { state: "saving" }, + parameters: { + docs: { + description: { + story: + "The shipped queue removes accepted responses optimistically while delivery remains pending; it has no separate visible progress indicator.", + }, + }, + }, + play: async ({ canvasElement }) => { + await userEvent.click( + await within(canvasElement).findByRole("button", { name: "Interrupt" }), + ); + await waitFor(() => + expect( + within(canvasElement).queryByTestId("task-chat-queued-messages"), + ).not.toBeInTheDocument(), + ); + await expect(canvasElement).toBeEmptyDOMElement(); + }, +}; +export const Delivered: Story = { + play: async ({ canvasElement }) => { + const c = within(canvasElement); + await userEvent.click(await c.findByRole("button", { name: "Interrupt" })); + await waitFor(() => + expect( + c.queryByText("Accepted: Review the migration"), + ).not.toBeInTheDocument(), + ); + }, +}; diff --git a/ui/storybook/stories/workspaces/workspace-close.stories.tsx b/ui/storybook/stories/workspaces/workspace-close.stories.tsx new file mode 100644 index 00000000000..8ba528a540d --- /dev/null +++ b/ui/storybook/stories/workspaces/workspace-close.stories.tsx @@ -0,0 +1,127 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, userEvent, within } from "storybook/test"; +import { ExecutionWorkspaceCloseDialog } from "@/components/ExecutionWorkspaceCloseDialog"; +import { ToastViewport } from "@/components/ToastViewport"; +import { + prepareFixtureApi, + type FixtureState, +} from "../../support/capability-gaps/api"; +function CloseWorkspace() { + const [open, setOpen] = useState(true); + return ( + <> + + + + ); +} +const meta = { + id: "feature-workspace-close", + title: + "Projects and execution/Workspaces, services, and files/Close readiness", + component: CloseWorkspace, + parameters: { layout: "padded" }, + beforeEach: ({ parameters }) => + prepareFixtureApi( + [ + { + path: "/api/execution-workspaces/workspace-gap/close-readiness", + read: () => ({ + workspaceId: "workspace-gap", + deliveryState: "unknown", + isDestructiveCloseAllowed: !parameters.blocked, + state: parameters.blocked ? "blocked" : "ready", + linkedIssues: [], + blockingReasons: parameters.blocked + ? ["A task is still running in this workspace."] + : [], + warnings: [], + runtimeServices: [], + plannedActions: [], + isSharedWorkspace: false, + isProjectPrimaryWorkspace: false, + git: null, + }), + }, + { + path: "/api/execution-workspaces/workspace-gap", + method: "PATCH", + read: ({ body }) => ({ + id: "workspace-gap", + companyId: "company-storybook", + name: "Feature review checkout", + ...body, + }), + }, + ], + parameters.fixtureState as FixtureState, + ).cleanup, +} satisfies Meta; +export default meta; +type Story = StoryObj; +export const Ready: Story = { + play: async () => { + await within(document.body).findByText("Close is ready"); + }, +}; +export const Loading: Story = { + parameters: { fixtureState: "loading" }, + play: async () => { + await within(document.body).findByText( + "Checking whether this workspace is safe to close...", + ); + }, +}; +export const FailedInspection: Story = { + parameters: { fixtureState: "failure" }, + play: async () => { + await within(document.body).findByText( + "The request failed. Please try again.", + ); + }, +}; +export const PermissionDenied: Story = { + parameters: { fixtureState: "permission" }, + play: async () => { + await within(document.body).findByText( + "You do not have permission to perform this action.", + ); + }, +}; +export const BlockedByRunningTask: Story = { + parameters: { blocked: true }, + play: async () => { + const c = within(document.body); + await c.findByText("Close is blocked"); + await expect( + c.getByRole("button", { name: "Close workspace" }), + ).toBeDisabled(); + }, +}; +export const Closing: Story = { + parameters: { fixtureState: "saving" }, + play: async () => { + const c = within(document.body); + await c.findByText("Close is ready"); + await userEvent.click(c.getByRole("button", { name: "Close workspace" })); + await expect( + c.getByRole("button", { name: "Close workspace" }), + ).toBeDisabled(); + }, +}; +export const Closed: Story = { + play: async () => { + const c = within(document.body); + await c.findByText("Close is ready"); + await userEvent.click(c.getByRole("button", { name: "Close workspace" })); + await c.findByText("Workspace closed"); + await expect(c.queryByRole("dialog")).not.toBeInTheDocument(); + }, +}; diff --git a/ui/storybook/support/capability-gaps/api.test.ts b/ui/storybook/support/capability-gaps/api.test.ts new file mode 100644 index 00000000000..3c110a897f9 --- /dev/null +++ b/ui/storybook/support/capability-gaps/api.test.ts @@ -0,0 +1,86 @@ +// @vitest-environment jsdom +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + installFixtureApi, + prepareFixtureApi, + activatePreparedFixtures, +} from "./api"; +let cleanup: (() => void) | undefined; +afterEach(() => { + cleanup?.(); + cleanup = undefined; + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + localStorage.clear(); +}); +describe("capability story API boundary", () => { + it("activates story routes after preview defaults and restores those defaults", async () => { + const preview = vi.fn(async () => Response.json({ source: "preview" })); + const fixture = prepareFixtureApi([ + { path: "/api/subject", read: () => ({ source: "story" }) }, + ]); + cleanup = fixture.cleanup; + vi.stubGlobal("fetch", preview); + activatePreparedFixtures(); + expect(await (await window.fetch("/api/subject")).json()).toEqual({ + source: "story", + }); + expect(await (await window.fetch("/api/other")).json()).toEqual({ + source: "preview", + }); + fixture.cleanup(); + cleanup = undefined; + expect(window.fetch).toBe(preview); + }); + it("blocks an unregistered mutation without calling the outer fetch", async () => { + const outer = vi + .spyOn(window, "fetch") + .mockResolvedValue(Response.json({ unexpected: true })); + cleanup = installFixtureApi([]).cleanup; + const reply = await window.fetch("/api/companies/company-storybook", { + method: "PATCH", + body: "{}", + }); + expect(reply.status).toBe(501); + expect(outer).not.toHaveBeenCalled(); + }); + it("parses Request bodies and records the mutation before a simulated denial", async () => { + const handler = vi.fn(() => ({ saved: true })); + const fixture = installFixtureApi( + [{ path: "/api/example", method: "POST", read: handler }], + "mutation-permission", + ); + cleanup = fixture.cleanup; + const result = await window.fetch( + new Request(`${location.origin}/api/example`, { + method: "POST", + body: JSON.stringify({ value: 42 }), + }), + ); + expect(result.status).toBe(403); + expect(fixture.requests[0].body).toEqual({ value: 42 }); + expect(handler).not.toHaveBeenCalled(); + }); + it("keeps prerequisite flags available during a subject error and restores fetch and company selection", async () => { + const previous = window.fetch; + localStorage.setItem("paperclip.selectedCompanyId", "original-company"); + const fixture = installFixtureApi( + [ + { path: "/api/flags", state: "ready", read: () => ({ enabled: true }) }, + { path: "/api/subject", read: () => [] }, + ], + "failure", + ); + cleanup = fixture.cleanup; + expect(await (await window.fetch("/api/flags")).json()).toEqual({ + enabled: true, + }); + expect((await window.fetch("/api/subject")).status).toBe(500); + fixture.cleanup(); + cleanup = undefined; + expect(window.fetch).toBe(previous); + expect(localStorage.getItem("paperclip.selectedCompanyId")).toBe( + "original-company", + ); + }); +}); diff --git a/ui/storybook/support/capability-gaps/api.ts b/ui/storybook/support/capability-gaps/api.ts new file mode 100644 index 00000000000..cb3a74ae97e --- /dev/null +++ b/ui/storybook/support/capability-gaps/api.ts @@ -0,0 +1,117 @@ +export type FixtureState = + | "ready" + | "loading" + | "empty" + | "failure" + | "permission" + | "saving" + | "mutation-failure" + | "mutation-permission"; +export interface FixtureRequest { + url: URL; + method: string; + body: Record; +} +export interface FixtureRoute { + path: string | RegExp; + method?: string; + state?: FixtureState; + read: ( + request: FixtureRequest, + ) => unknown | Response | Promise; +} +const pendingFixtures: Array<() => void> = []; +export function activatePreparedFixtures() { + for (const activate of pendingFixtures.splice(0)) activate(); +} +export function prepareFixtureApi( + routes: FixtureRoute[], + state: FixtureState = "ready", +) { + return installFixtureApi(routes, state, true); +} +export function installFixtureApi( + routes: FixtureRoute[], + state: FixtureState = "ready", + deferred = false, +) { + let previous = window.fetch; + const selectedCompany = localStorage.getItem("paperclip.selectedCompanyId"); + localStorage.setItem("paperclip.selectedCompanyId", "company-storybook"); + const requests: FixtureRequest[] = []; + const fixtureFetch: typeof fetch = async (input, init) => { + const request = input instanceof Request ? input : null; + const url = new URL(request?.url ?? String(input), location.origin); + const method = (init?.method ?? request?.method ?? "GET").toUpperCase(); + const route = routes.find( + (candidate) => + (typeof candidate.path === "string" + ? candidate.path === url.pathname + : candidate.path.test(url.pathname)) && + (!candidate.method || candidate.method === method), + ); + if (!route) { + if (url.pathname.startsWith("/api/") && method !== "GET") { + return Response.json( + { error: `No isolated fixture for ${method} ${url.pathname}` }, + { status: 501 }, + ); + } + return previous(input, init); + } + const routeState = route.state ?? state; + const rawBody = + init?.body ?? + (request && method !== "GET" ? await request.clone().text() : null); + const body = rawBody + ? (JSON.parse(String(rawBody)) as Record) + : {}; + const fixtureRequest = { url, method, body }; + requests.push(fixtureRequest); + if ( + (routeState === "loading" && method === "GET") || + (routeState === "saving" && method !== "GET") + ) { + return new Promise(() => {}); + } + if ( + routeState === "failure" || + routeState === "permission" || + (method !== "GET" && + (routeState === "mutation-failure" || + routeState === "mutation-permission")) + ) { + const denied = + routeState === "permission" || routeState === "mutation-permission"; + return Response.json( + { + error: denied + ? "You do not have permission to perform this action." + : "The request failed. Please try again.", + }, + { status: denied ? 403 : 500 }, + ); + } + const result = await route.read(fixtureRequest); + return result instanceof Response ? result : Response.json(result); + }; + let active = !deferred; + const activate = () => { + previous = window.fetch; + window.fetch = fixtureFetch; + active = true; + }; + if (deferred) pendingFixtures.push(activate); + else window.fetch = fixtureFetch; + return { + requests, + cleanup: () => { + const pendingIndex = pendingFixtures.indexOf(activate); + if (pendingIndex >= 0) pendingFixtures.splice(pendingIndex, 1); + if (active) window.fetch = previous; + if (selectedCompany === null) + localStorage.removeItem("paperclip.selectedCompanyId"); + else localStorage.setItem("paperclip.selectedCompanyId", selectedCompany); + }, + }; +} diff --git a/ui/storybook/support/capability-gaps/cases.ts b/ui/storybook/support/capability-gaps/cases.ts new file mode 100644 index 00000000000..30f147f10ce --- /dev/null +++ b/ui/storybook/support/capability-gaps/cases.ts @@ -0,0 +1,114 @@ +import { instanceExperimentalSettingsSchema } from "@paperclipai/shared"; +import type { CaseSummary, CaseDetail, CaseEvent } from "@/api/cases"; +import { prepareFixtureApi, type FixtureState } from "./api"; +export const caseItem: CaseSummary = { + id: "case-gap", + companyId: "company-storybook", + projectId: null, + caseNumber: 42, + identifier: "PAP-C42", + caseType: "release", + key: "release-preview", + title: "Release readiness", + summary: "The release review record.", + status: "in_review", + fields: { + version: "0.3.1", + ready: false, + checks: { build: "passed", tests: "pending" }, + task_identifiers: ["PAP-42"], + }, + parentCaseId: null, + createdByAgentId: null, + createdByUserId: null, + completedAt: null, + createdAt: "2026-10-12T12:00:00Z", + updatedAt: "2026-10-12T12:00:00Z", +}; +export const childCase: CaseSummary = { + ...caseItem, + id: "child-gap", + caseNumber: 43, + identifier: "PAP-C43", + title: "API qualification", + parentCaseId: caseItem.id, + status: "in_progress", +}; +export const caseDetail: CaseDetail = { + ...caseItem, + parent: null, + labels: [], + documents: [], + attachments: [], + issueLinks: [ + { + id: "link-gap", + caseId: caseItem.id, + issueId: "task-gap", + role: "work", + createdAt: caseItem.createdAt, + issue: { + id: "task-gap", + identifier: "PAP-42", + title: "Qualify the release", + status: "in_progress", + }, + }, + ], +}; +const event: CaseEvent = { + id: "event-gap", + caseId: caseItem.id, + kind: "status_changed", + actorType: "agent", + actorAgentId: "agent-coder", + actorUserId: null, + runId: "run-gap", + payload: { from: "in_progress", to: "in_review" }, + createdAt: caseItem.updatedAt, + actorAgentName: "Codex Coder", + issue: caseDetail.issueLinks[0].issue, +}; +export function installCaseFixtures(state: FixtureState = "ready") { + return prepareFixtureApi( + [ + { + path: "/api/instance/settings/experimental", + state: "ready", + read: () => ({ + ...instanceExperimentalSettingsSchema.parse({}), + enableCases: true, + }), + }, + { + path: "/api/companies/company-storybook/cases", + read: ({ url }) => + state === "empty" + ? [] + : url.searchParams.has("parent") + ? [childCase] + : [caseItem, childCase], + }, + { path: /^\/api\/cases\/(PAP-C42|case-gap)$/, read: () => caseDetail }, + { + path: /^\/api\/cases\/(PAP-C42|case-gap)\/events$/, + read: () => (state === "empty" ? [] : [event]), + }, + { + path: "/api/issues/task-gap/cases", + read: () => + state === "empty" + ? [] + : [ + { + id: "reverse-link-gap", + role: "work", + createdAt: caseItem.createdAt, + case: caseItem, + }, + ], + }, + ], + state, + ).cleanup; +} diff --git a/ui/storybook/support/capability-gaps/environments.ts b/ui/storybook/support/capability-gaps/environments.ts new file mode 100644 index 00000000000..b72fc2bd995 --- /dev/null +++ b/ui/storybook/support/capability-gaps/environments.ts @@ -0,0 +1,172 @@ +import type { Environment, EnvironmentCapabilities } from "@paperclipai/shared"; +import { prepareFixtureApi, type FixtureState } from "./api"; +export const environment: Environment = { + id: "environment-gap", + name: "Release sandbox", + description: "Isolated release checks", + driver: "sandbox", + status: "active", + config: { provider: "daytona", reuseLease: true }, + envVars: {}, + metadata: null, + createdAt: new Date("2026-10-12"), + updatedAt: new Date("2026-10-12"), +}; +const capabilities: EnvironmentCapabilities = { + adapters: [], + drivers: { + local: "supported", + ssh: "supported", + sandbox: "supported", + plugin: "unsupported", + }, + sandboxProviders: { + fake: { + status: "unsupported", + supportsLoginPty: false, + supportsSavedProbe: false, + supportsUnsavedProbe: false, + supportsRunExecution: false, + supportsReusableLeases: false, + supportsInteractiveSetup: false, + interactiveSetupConnectionTypes: [], + supportsTemplateCapture: false, + supportsTemplateDelete: false, + }, + daytona: { + status: "supported", + supportsLoginPty: true, + supportsSavedProbe: true, + supportsUnsavedProbe: true, + supportsRunExecution: true, + supportsReusableLeases: true, + supportsInteractiveSetup: true, + interactiveSetupConnectionTypes: ["ssh"], + supportsTemplateCapture: true, + supportsTemplateDelete: true, + displayName: "Daytona", + }, + }, +}; +export function installEnvironmentFixture( + state: FixtureState = "ready", + image: "none" | "captured" | "failed" | "loading" | "denied" = "none", + unsupported = false, +) { + let current = { ...environment }; + const template = { + id: "template-gap", + environmentId: environment.id, + provider: "daytona", + templateKind: "snapshot", + templateRef: "fixture-template", + sourceTemplateRef: null, + sourceEnvironmentConfigFingerprint: "fixture-fingerprint", + status: "active", + createdByUserId: "user-storybook", + createdByAgentId: null, + capturedAt: "2026-10-12T12:00:00Z", + lastUsedAt: null, + supersededByTemplateId: null, + metadata: null, + createdAt: "2026-10-12T12:00:00Z", + updatedAt: "2026-10-12T12:00:00Z", + }; + return prepareFixtureApi( + [ + { + path: "/api/instance/settings/experimental", + state: "ready", + read: () => ({ + enableEnvironments: true, + enableManagedSandboxOnly: false, + }), + }, + { + path: "/api/instance/settings", + state: "ready", + read: () => ({ defaultEnvironmentId: environment.id }), + }, + { + path: "/api/companies/company-storybook/environments", + read: () => (state === "empty" ? [] : [current]), + }, + { + path: "/api/companies/company-storybook/environments/capabilities", + read: () => + unsupported + ? { + ...capabilities, + sandboxProviders: { + fake: { + status: "unsupported", + supportsLoginPty: false, + supportsSavedProbe: false, + supportsUnsavedProbe: false, + supportsRunExecution: false, + supportsReusableLeases: false, + supportsInteractiveSetup: false, + interactiveSetupConnectionTypes: [], + supportsTemplateCapture: false, + supportsTemplateDelete: false, + }, + daytona: { + ...capabilities.sandboxProviders.daytona, + status: "unsupported", + supportsSavedProbe: false, + supportsTemplateCapture: false, + }, + }, + } + : capabilities, + }, + { + path: "/api/environments/environment-gap/secret-refs", + read: () => ({ refs: [] }), + }, + { + path: "/api/environments/environment-gap", + method: "PATCH", + read: ({ body }) => { + current = { ...current, ...body }; + return current; + }, + }, + { + path: "/api/environments/environment-gap/probe", + method: "POST", + read: () => ({ + ok: true, + driver: "sandbox", + summary: "Provider connection succeeded.", + details: {}, + }), + }, + { + path: "/api/environments/environment-gap/custom-image-template", + read: () => + image === "loading" + ? new Promise(() => {}) + : image === "denied" + ? Response.json( + { + error: "You do not have permission to inspect this image.", + }, + { status: 403 }, + ) + : image === "failed" + ? Response.json( + { error: "Image metadata is unavailable." }, + { status: 503 }, + ) + : { + activeTemplate: image === "captured" ? template : null, + activeTemplateMatchesConfig: true, + activeSession: null, + latestSession: null, + }, + }, + ], + state, + ); +} diff --git a/ui/storybook/support/capability-gaps/live-events.ts b/ui/storybook/support/capability-gaps/live-events.ts new file mode 100644 index 00000000000..a2f230934cd --- /dev/null +++ b/ui/storybook/support/capability-gaps/live-events.ts @@ -0,0 +1,53 @@ +import type { LiveEvent } from "@paperclipai/shared"; +const sockets: FixtureSocket[] = []; +const listeners = new Set<() => void>(); +class FixtureSocket { + static CONNECTING = 0; + static OPEN = 1; + static CLOSING = 2; + static CLOSED = 3; + readyState = 0; + onopen: ((event: Event) => void) | null = null; + onmessage: ((event: MessageEvent) => void) | null = null; + onclose: ((event: CloseEvent) => void) | null = null; + onerror: ((event: Event) => void) | null = null; + constructor() { + sockets.push(this); + queueMicrotask(() => { + if (this.readyState === 3) return; + this.readyState = 1; + this.onopen?.(new Event("open")); + for (const listener of listeners) listener(); + }); + } + close() { + this.readyState = 3; + } + send() {} +} +export function listenForFixtureSocket(listener: () => void) { + listeners.add(listener); + if (sockets.some((s) => s.readyState === 1)) listener(); + return () => { + listeners.delete(listener); + }; +} +export function publishFixtureEvent(event: LiveEvent) { + for (const socket of sockets) + if (socket.readyState === 1) + socket.onmessage?.( + new MessageEvent("message", { data: JSON.stringify(event) }), + ); +} +export function installLiveEventFixture() { + const previous = window.WebSocket; + sockets.splice(0); + listeners.clear(); + window.WebSocket = FixtureSocket as unknown as typeof WebSocket; + return () => { + for (const socket of sockets) socket.close(); + sockets.splice(0); + listeners.clear(); + window.WebSocket = previous; + }; +} diff --git a/ui/storybook/support/capability-gaps/pipelines.ts b/ui/storybook/support/capability-gaps/pipelines.ts new file mode 100644 index 00000000000..380d20b3948 --- /dev/null +++ b/ui/storybook/support/capability-gaps/pipelines.ts @@ -0,0 +1,201 @@ +import type { + PipelineDetail, + PipelineCase, + PipelineReviewCaseRow, + PipelineCompanyCaseEvent, +} from "@/api/pipelines"; +import { prepareFixtureApi, type FixtureState } from "./api"; +export const pipeline: PipelineDetail = { + id: "pipeline-gap", + companyId: "company-storybook", + key: "release", + name: "Release review", + description: "Check and approve a release.", + projectId: null, + enforceTransitions: true, + archivedAt: null, + stageCount: 3, + openCaseCount: 1, + createdAt: "2026-10-12", + updatedAt: "2026-10-12", + stages: [ + { + id: "stage-work", + pipelineId: "pipeline-gap", + key: "work", + name: "Prepare", + kind: "working", + position: 0, + }, + { + id: "stage-review", + pipelineId: "pipeline-gap", + key: "review", + name: "Review", + kind: "review", + position: 1, + config: { + approveToStageKey: "done", + requestChangesToStageKey: "work", + requireRequestChangesReason: true, + }, + }, + { + id: "stage-done", + pipelineId: "pipeline-gap", + key: "done", + name: "Done", + kind: "done", + position: 2, + }, + ], + transitions: [ + { fromStageId: "stage-work", toStageId: "stage-review" }, + { fromStageId: "stage-review", toStageId: "stage-done" }, + ], +}; +export function installPipelineFixture( + state: FixtureState = "ready", + options: { emptyStages?: boolean } = {}, +) { + let item: PipelineCase = { + id: "case-pipeline-gap", + companyId: "company-storybook", + pipelineId: pipeline.id, + stageId: "stage-review", + caseKey: "REL-1", + title: "Ship the release notes", + version: 1, + fields: { release: "2026.1013.0" }, + createdAt: "2026-10-12", + updatedAt: "2026-10-12", + }; + let review = true; + const detail = () => ({ + case: item, + stage: pipeline.stages.find((s) => s.id === item.stageId), + pipeline, + allowedNextStages: [pipeline.stages[2]], + links: [], + blockers: [], + blocks: [], + childrenSummary: { + childCount: 0, + terminalChildCount: 0, + loadedChildren: 0, + }, + }); + const event: PipelineCompanyCaseEvent = { + id: "event-gap", + companyId: "company-storybook", + caseId: item.id, + type: "review_decided", + actorType: "user", + actorUserId: "user-storybook", + fromStageId: "stage-review", + toStageId: "stage-done", + fromStage: pipeline.stages[1], + toStage: pipeline.stages[2], + payload: { + decision: "approve", + actorName: "Release reviewer", + reason: "All release checks passed.", + }, + createdAt: "2026-10-12T12:00:00Z", + updatedAt: "2026-10-12T12:00:00Z", + case: { + id: item.id, + caseKey: item.caseKey ?? null, + title: item.title, + terminalKind: null, + }, + pipeline: { id: pipeline.id, key: pipeline.key, name: pipeline.name }, + }; + const reviewRow = (): PipelineReviewCaseRow => ({ + case: item, + pipeline: { id: pipeline.id, key: pipeline.key, name: pipeline.name }, + stage: pipeline.stages[1], + reviewConfig: { + approveToStageKey: "done", + requestChangesToStageKey: "work", + requireRequestChangesReason: true, + }, + }); + return prepareFixtureApi( + [ + { + path: "/api/companies/company-storybook/pipelines", + read: () => (state === "empty" ? [] : [pipeline]), + }, + { + path: "/api/pipelines/pipeline-gap", + read: () => + options.emptyStages + ? { ...pipeline, stages: [], stageCount: 0, transitions: [] } + : pipeline, + }, + { + path: "/api/pipelines/pipeline-gap/cases", + read: () => + state === "empty" ? [] : [{ case: item, stage: detail().stage }], + }, + { + path: "/api/pipelines/pipeline-gap/health", + read: () => ({ warnings: [], pipelineId: pipeline.id }), + }, + { + path: "/api/pipelines/pipeline-gap/intake-form", + read: () => ({ + pipelineId: pipeline.id, + stageId: "stage-work", + fields: [], + }), + }, + { + path: "/api/companies/company-storybook/pipelines-attention", + read: () => ({ + suggestions: [], + reviews: [], + headsUp: [], + counts: { suggestions: 0, reviews: 0, headsUp: 0 }, + }), + }, + { + path: "/api/companies/company-storybook/review-cases", + read: () => (state === "empty" || !review ? [] : [reviewRow()]), + }, + { path: "/api/cases/case-pipeline-gap", read: detail }, + { + path: /^\/api\/cases\/case-pipeline-gap\/(children|events|issue-links)$/, + read: () => [], + }, + { + path: "/api/cases/case-pipeline-gap/review", + method: "POST", + read: ({ body }) => { + review = false; + item = { + ...item, + stageId: body.decision === "approve" ? "stage-done" : "stage-work", + version: 2, + }; + return detail(); + }, + }, + { + path: "/api/companies/company-storybook/case-events", + read: () => ({ + items: state === "empty" ? [] : [event], + pagination: { + limit: 50, + offset: 0, + nextOffset: null, + hasMore: false, + order: "desc", + }, + }), + }, + ], + state, + ); +} diff --git a/ui/storybook/support/capability-gaps/plugins.ts b/ui/storybook/support/capability-gaps/plugins.ts new file mode 100644 index 00000000000..5a23cd4152d --- /dev/null +++ b/ui/storybook/support/capability-gaps/plugins.ts @@ -0,0 +1,110 @@ +import { prepareFixtureApi, type FixtureState } from "./api"; +export function installPluginFixtures( + state: FixtureState = "ready", + empty = false, + pluginId = "plugin-gap", +) { + let plugin = { + id: pluginId, + pluginKey: "storybook.fixture", + packageName: "@paperclipai/storybook-fixture", + version: "1.0.0", + status: "ready", + categories: ["automation"], + manifestJson: { + displayName: "Preview plugin", + version: "1.0.0", + description: "An isolated plugin fixture.", + capabilities: [], + instanceConfigSchema: { + type: "object", + properties: { label: { type: "string", title: "Label" } }, + required: ["label"], + }, + }, + lastError: null as string | null, + packagePath: null, + createdAt: "2026-10-12T12:00:00Z", + updatedAt: "2026-10-12T12:00:00Z", + }; + let rows = empty ? [] : [plugin]; + let configJson: Record = { label: "Release preview" }; + const health = { + pluginId: plugin.id, + status: "ready", + healthy: true, + checks: [], + }; + return prepareFixtureApi( + [ + { path: "/api/plugins", read: () => rows }, + { path: "/api/plugins/examples", read: () => [] }, + { path: "/api/plugins/ui-contributions", read: () => [] }, + { + path: "/api/plugins/install", + method: "POST", + read: () => { + rows = [plugin]; + return plugin; + }, + }, + { + path: `/api/plugins/${pluginId}`, + read: ({ method }) => { + if (method === "DELETE") { + rows = []; + return { ok: true }; + } + return plugin; + }, + }, + { + path: new RegExp(`^/api/plugins/${pluginId}/(enable|disable)$`), + method: "POST", + read: ({ url }) => { + plugin = { + ...plugin, + status: url.pathname.endsWith("disable") ? "disabled" : "ready", + lastError: url.pathname.endsWith("disable") + ? "Disabled by operator" + : null, + }; + rows = [plugin]; + return { ok: true }; + }, + }, + { path: `/api/plugins/${pluginId}/health`, read: () => health }, + { path: `/api/plugins/${pluginId}/logs`, read: () => [] }, + { + path: `/api/plugins/${pluginId}/dashboard`, + read: () => ({ + pluginId: plugin.id, + worker: null, + recentJobRuns: [], + recentWebhookDeliveries: [], + health, + checkedAt: "2026-10-12T12:00:00Z", + }), + }, + { + path: `/api/plugins/${pluginId}/config`, + read: ({ method, body }) => { + if (method === "POST") + configJson = body.configJson as Record; + return { + id: "config-gap", + pluginId: plugin.id, + companyId: "company-storybook", + configJson, + }; + }, + }, + { + path: `/api/plugins/${pluginId}/config/test`, + method: "POST", + read: () => ({ valid: true }), + }, + ], + state, + ).cleanup; +} diff --git a/ui/storybook/support/capability-gaps/skills.ts b/ui/storybook/support/capability-gaps/skills.ts new file mode 100644 index 00000000000..65534566b04 --- /dev/null +++ b/ui/storybook/support/capability-gaps/skills.ts @@ -0,0 +1,49 @@ +import type { CompanySkillDetail } from "@paperclipai/shared"; +export function makeSkill( + overrides: Partial = {}, +): CompanySkillDetail { + return { + id: "skill-gap", + companyId: "company-storybook", + key: "paperclip/demo-skill", + slug: "demo-skill", + name: "Demo Skill", + description: "A demo skill.", + markdown: + "---\nname: Demo Skill\ndescription: Existing\n---\n\n# Demo Skill\n", + sourceType: "local_path", + sourceLocator: null, + sourceRef: null, + trustLevel: "markdown_only", + compatibility: "compatible", + fileInventory: [{ path: "SKILL.md", kind: "skill" }], + iconUrl: null, + color: null, + tagline: "Existing tagline", + authorName: null, + homepageUrl: null, + categories: ["engineering"], + sharingScope: "company", + publicShareToken: null, + forkedFromSkillId: null, + forkedFromCompanyId: null, + starCount: 0, + installCount: 0, + forkCount: 0, + currentVersionId: null, + metadata: null, + createdAt: new Date("2026-01-01T00:00:00Z"), + updatedAt: new Date("2026-01-02T00:00:00Z"), + attachedAgentCount: 0, + usedByAgents: [], + editable: true, + editableReason: null, + sourceLabel: "Local", + sourceBadge: "local", + sourcePath: null, + currentVersion: null, + starredByCurrentActor: false, + existingForks: [], + ...overrides, + }; +} diff --git a/ui/storybook/support/capability-gaps/status-cards.ts b/ui/storybook/support/capability-gaps/status-cards.ts new file mode 100644 index 00000000000..31db584dd0d --- /dev/null +++ b/ui/storybook/support/capability-gaps/status-cards.ts @@ -0,0 +1,54 @@ +import type { StatusCard } from "@paperclipai/shared"; +import type { StatusCardView } from "@/pages/StatusCards/types"; +export function makeStatusCard( + overrides: Partial = {}, +): StatusCardView { + const card: StatusCard = { + id: "card-gap", + companyId: "company-storybook", + createdByUserId: null, + createdByAgentId: null, + title: "Release readiness", + titlePinned: false, + interestPrompt: "issues that help ship the new install subcommand", + queries: [], + queryVersion: 1, + queryCompiledAt: "2026-07-22T10:00:00.000Z", + queryCompiledByAgentId: null, + agentId: null, + refreshPolicy: { + mode: "interval", + intervalMinutes: 15, + triggers: { + statusTransitions: true, + membershipChanges: true, + humanComments: true, + assigneeChanges: true, + anyUpdate: false, + }, + }, + state: "active", + pendingChangeCount: 0, + lastChangeAt: null, + fingerprint: null, + fingerprintAt: null, + mentionedIssueIds: [], + documentId: null, + lastUpdateRunKind: "full", + lastGeneratedAt: "2026-07-22T11:00:00.000Z", + lastModel: "claude-haiku", + generatingIssueId: null, + failureReason: null, + nextEvalAt: null, + archivedAt: null, + archivedByUserId: null, + archivedByAgentId: null, + createdAt: "2026-07-22T09:00:00.000Z", + updatedAt: "2026-07-22T11:00:00.000Z", + }; + return { + ...card, + summaryBody: "All on track. Next: review the deep-link fix, then merge.", + ...overrides, + }; +}